@reblu/site-next 1.14.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/route-handlers.ts","../src/shop/route-handlers.ts"],"sourcesContent":["/**\n * `@reblu/site-next/route-handlers` — server-only, dependency-free Route\n * Handler factory (REB-510 / design doc \"site-auth-package\").\n *\n * This module MUST stay dependency-free (no `@reblu/site-ui`, no React): it is\n * imported directly by `app/api/*` Route Handler files, and its own tsup entry\n * carries NO `'use client'` directive (mirrors the `cache` subpath's R4\n * precedent for the opposite direction — see `tsup.config.ts`).\n *\n * Scope: this factory covers the `/api/site/auth/*` proxy shape specifically\n * (fixed target path, JSON body passthrough, optional `Set-Cookie` forward) —\n * the same shape as every one of demo-site's 8 auth Route Handlers. It is\n * modeled on `apps/demo-site/src/lib/api/proxy.ts` (`proxyToBaseApi`) but is\n * NOT a drop-in replacement for every one of that helper's call sites: it\n * does not support a per-request DYNAMIC `targetPath` (e.g.\n * `api/shop/order/[sessionId]/route.ts`'s `` `/api/site/shop/order/${id}` ``)\n * or an arbitrary extra-`headers` passthrough — both are genuinely out of\n * scope for REB-510 (auth only). demo-site's non-auth proxy routes keep using\n * `proxyToBaseApi` directly; only the 8 auth routes are candidates to adopt\n * this factory.\n *\n * export const POST = createProxyRoute('/api/site/auth/login')\n *\n * Deliberate behavior change vs the original (R6): `proxy.ts` reads\n * `REBLU_API_BASE_URL`/`REBLU_API_KEY` at MODULE LOAD. This factory reads them\n * LAZILY, inside the returned handler, on every request — `opts.baseUrl` /\n * `opts.apiKey` still take priority when provided.\n *\n * Why this factory reads `process.env` at all, unlike `@reblu/site-client`\n * (whose own docblock states \"the SDK never reads `process.env` itself\"):\n * `site-client` is instantiated ONCE by the consuming SITE, which already\n * owns and injects its env at that single call site. `createProxyRoute` is\n * the opposite shape on purpose — a ZERO-CONFIG one-liner meant to be called\n * once per Route Handler FILE (8+ files in a typical SITE), so requiring\n * every file to re-thread `baseUrl`/`apiKey` would defeat the \"one line\"\n * value proposition documented in the README. The env fallback is this\n * factory's deliberate ergonomic default; `opts.baseUrl`/`opts.apiKey` remain\n * available for a SITE that wants explicit, non-`process.env` control.\n *\n * Trust note on header forwarding: `X-Forwarded-For`/`X-Real-IP` are copied\n * from the incoming request VERBATIM (same as the original `proxy.ts`). On\n * Vercel the platform itself sets/overwrites these ahead of the Route\n * Handler, so they're trustworthy there — but this is a distributable SDK a\n * SITE can deploy anywhere. On a host where nothing strips inbound spoofed\n * values, a client can forge the IP base-api rate-limits on. Deploy this\n * factory only behind a platform that normalizes/overwrites these headers,\n * or in front of infra that strips client-supplied ones.\n */\nimport type { NextRequest } from 'next/server'\n\nexport interface CreateProxyRouteOptions {\n /** Overrides `REBLU_API_BASE_URL`. Read lazily per-request when omitted. */\n baseUrl?: string\n /** Overrides `REBLU_API_KEY`. Read lazily per-request when omitted. */\n apiKey?: string\n /** @default 'POST' */\n method?: string\n /** Forward upstream `Set-Cookie` headers to the browser. @default false */\n forwardSetCookie?: boolean\n /**\n * Reject non-`application/json` requests with a Spanish 400 before ever\n * reaching the network — mirrors `magic-link-exchange/route.ts`'s guard.\n * @default false\n */\n requireJson?: boolean\n /**\n * Overrides the Spanish `message` field of the `requireJson` 400 body, for\n * a non-Spanish-default SITE. The `error` code (`invalid_request`) and\n * shape stay fixed — only the human-facing text is overridable.\n * @default 'Solicitud no válida'\n */\n invalidRequestMessage?: string\n /**\n * Hard timeout (ms) for the upstream fetch, enforced via `AbortController`\n * — mirrors `@reblu/site-client`'s own `timeoutMs` (default `8000`, same\n * value). Without this, a hung base-api response would hang the SITE's\n * Route Handler indefinitely instead of degrading.\n * @default 8000\n */\n timeoutMs?: number\n /**\n * Overrides the `message` field of the 500 body returned when neither\n * `opts.baseUrl`/`opts.apiKey` nor `REBLU_API_BASE_URL`/`REBLU_API_KEY`\n * resolve to a non-empty value.\n * @default 'Error de configuración del servidor.'\n */\n configurationErrorMessage?: string\n /**\n * Overrides the `message` field of the 502 body returned when the\n * upstream fetch times out.\n * @default 'La solicitud ha superado el tiempo de espera.'\n */\n timeoutMessage?: string\n /**\n * Overrides the `message` field of the 502 body returned when the\n * upstream fetch fails for any other reason (DNS, connection refused,\n * etc.).\n * @default 'Error de conexión con el servidor.'\n */\n proxyErrorMessage?: string\n}\n\nfunction jsonErrorResponse(status: number, error: string, message: string): Response {\n return new Response(\n JSON.stringify({ error, message, correlation_id: crypto.randomUUID() }),\n { status, headers: { 'Content-Type': 'application/json; charset=utf-8' } },\n )\n}\n\n/**\n * Creates a Next.js Route Handler that proxies `request` to\n * `${baseUrl}${targetPath}` at base-api, injecting the `X-Reblu-Api-Key`\n * server-side so it never reaches the browser.\n */\nexport function createProxyRoute(targetPath: string, opts: CreateProxyRouteOptions = {}) {\n const {\n method = 'POST',\n forwardSetCookie = false,\n requireJson = false,\n invalidRequestMessage = 'Solicitud no válida',\n timeoutMs = 8000,\n configurationErrorMessage = 'Error de configuración del servidor.',\n timeoutMessage = 'La solicitud ha superado el tiempo de espera.',\n proxyErrorMessage = 'Error de conexión con el servidor.',\n } = opts\n\n return async function handler(request: NextRequest): Promise<Response> {\n if (requireJson) {\n const contentType = request.headers.get('content-type') ?? ''\n if (!contentType.includes('application/json')) {\n return jsonErrorResponse(400, 'invalid_request', invalidRequestMessage)\n }\n }\n\n // Lazy, per-request env read (R6) — opts win when provided.\n const baseUrl = opts.baseUrl ?? process.env.REBLU_API_BASE_URL ?? ''\n const apiKey = opts.apiKey ?? process.env.REBLU_API_KEY ?? ''\n\n if (!baseUrl || !apiKey) {\n console.error(\n `[createProxyRoute] Missing configuration for ${targetPath}: ` +\n `${!baseUrl ? 'baseUrl/REBLU_API_BASE_URL ' : ''}${!apiKey ? 'apiKey/REBLU_API_KEY' : ''}`.trim(),\n )\n return jsonErrorResponse(500, 'missing_configuration', configurationErrorMessage)\n }\n\n const forwardedHeaders: Record<string, string> = {}\n const ua = request.headers.get('user-agent')\n if (ua) forwardedHeaders['user-agent'] = ua\n const xff = request.headers.get('x-forwarded-for')\n if (xff) forwardedHeaders['x-forwarded-for'] = xff\n const xri = request.headers.get('x-real-ip')\n if (xri) forwardedHeaders['x-real-ip'] = xri\n\n const upperMethod = method.toUpperCase()\n const body =\n upperMethod === 'GET' || upperMethod === 'HEAD' ? undefined : await request.text()\n // Forward the original query string (e.g. `?slug=x`) regardless of\n // method — `targetPath` is fixed at factory-call time, so without this a\n // GET route configured via `{ method: 'GET' }` would silently drop it.\n const search = request.nextUrl?.search ?? ''\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n\n try {\n const res = await fetch(`${baseUrl}${targetPath}${search}`, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Reblu-Api-Key': apiKey,\n ...forwardedHeaders,\n },\n ...(body ? { body } : {}),\n cache: 'no-store',\n signal: controller.signal,\n })\n\n const data = await res.text()\n const responseHeaders = new Headers({\n 'Content-Type': res.headers.get('Content-Type') ?? 'application/json',\n })\n\n if (forwardSetCookie) {\n for (const cookie of res.headers.getSetCookie()) {\n responseHeaders.append('set-cookie', cookie)\n }\n }\n\n return new Response(data, { status: res.status, headers: responseHeaders })\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n console.error(`[createProxyRoute] Timed out forwarding to ${targetPath} after ${timeoutMs}ms`)\n return jsonErrorResponse(502, 'upstream_timeout', timeoutMessage)\n }\n console.error(`[createProxyRoute] Error forwarding to ${targetPath}:`, err)\n return jsonErrorResponse(502, 'proxy_error', proxyErrorMessage)\n } finally {\n clearTimeout(timer)\n }\n }\n}\n","/**\n * `@reblu/site-next/shop-route-handlers` (REB-829) — Route Handler factories\n * for the cart/checkout write flow (`/api/site/shop/{checkout, validate-cart,\n * apply-discount, check-email, order/[sessionId]}`). Every demo-site route\n * these replace (`app/api/shop/*`) either hand-rolled the SAME proxy\n * boilerplate `./route-handlers`'s `createProxyRoute` already generalizes\n * (checkout/validate-cart/apply-discount/check-email), or called\n * `proxyToBaseApi` directly for the one dynamic-path route (`order/\n * [sessionId]`) that `createProxyRoute` deliberately does not cover (its own\n * docblock: \"a per-request DYNAMIC targetPath... is genuinely out of scope\").\n * This module is the dynamic-path extension `createProxyRoute` named as a\n * gap, kept as its own file (not folded into `./server`, REB-809's filters/\n * collection glue) because it is a distinct concern — a page that only reads\n * shop data never needs these factories, and a cart/checkout integration\n * never needs the filters glue.\n *\n * Server-only, dependency-free (only `createProxyRoute` + the `next/server`\n * type) — same isolation direction as `./route-handlers`/`./shop/server.ts`:\n * this entry must NEVER carry `'use client'` (see `tsup.config.ts`).\n *\n * Mirrors `createProxyRoute`'s own API-key/server-only handling verbatim —\n * `REBLU_API_KEY` is read lazily, server-side, and injected as\n * `X-Reblu-Api-Key` on the upstream fetch only; it never reaches the\n * Response these handlers return to the browser.\n *\n * // app/api/shop/checkout/route.ts\n * export const POST = createShopCheckoutRoute()\n *\n * // app/api/shop/order/[sessionId]/route.ts\n * export const GET = createShopOrderRoute()\n */\nimport type { NextRequest } from 'next/server'\nimport { createProxyRoute, type CreateProxyRouteOptions } from '../route-handlers'\n\n/** Options for a fixed-path write route. `method` is always `'POST'` here. */\nexport type ShopWriteRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `requireJson: true` by default for every write route below: the browser\n * clients these proxy for (the cart/checkout UI) always set\n * `Content-Type: application/json`, and rejecting anything else with a 400\n * BEFORE the upstream fetch is strictly safer than forwarding an\n * unparseable body to base-api. A SITE with a non-JSON caller can still pass\n * `{ requireJson: false }`.\n */\nconst DEFAULT_WRITE_OPTS: ShopWriteRouteOptions = { requireJson: true }\n\n/** `POST /api/site/shop/checkout` — starts a hosted Stripe Checkout session. */\nexport function createShopCheckoutRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/checkout', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/validate-cart` — live stock/price re-check before checkout. */\nexport function createShopValidateCartRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/validate-cart', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/apply-discount` — validates a discount code against the cart. */\nexport function createShopApplyDiscountRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/apply-discount', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/check-email` — whether a guest's email already has an account. */\nexport function createShopCheckEmailRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/check-email', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** Options for the dynamic order-lookup route. `method` is always `'GET'`. */\nexport type ShopOrderRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `GET /api/site/shop/order/[sessionId]` — the one route in this file with a\n * per-request dynamic target path, so it can't be a one-line\n * `createProxyRoute(...)` call like the others: it reads Next's route\n * `params`, builds `/api/site/shop/order/{sessionId}` per request, and\n * delegates everything else (env, timeout, forwarding, error shaping) to\n * `createProxyRoute`.\n */\nexport function createShopOrderRoute(opts: ShopOrderRouteOptions = {}) {\n return async function handler(\n request: NextRequest,\n context: { params: Promise<{ sessionId: string }> },\n ): Promise<Response> {\n const { sessionId } = await context.params\n const targetPath = `/api/site/shop/order/${encodeURIComponent(sessionId)}`\n return createProxyRoute(targetPath, { ...opts, method: 'GET' })(request)\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/route-handlers.ts","../src/shop/checkout-redirect.ts","../src/shop/route-handlers.ts"],"sourcesContent":["/**\n * `@reblu/site-next/route-handlers` — server-only, dependency-free Route\n * Handler factory (REB-510 / design doc \"site-auth-package\").\n *\n * This module MUST stay dependency-free (no `@reblu/site-ui`, no React): it is\n * imported directly by `app/api/*` Route Handler files, and its own tsup entry\n * carries NO `'use client'` directive (mirrors the `cache` subpath's R4\n * precedent for the opposite direction — see `tsup.config.ts`).\n *\n * Scope: this factory covers the `/api/site/auth/*` proxy shape specifically\n * (fixed target path, JSON body passthrough, optional `Set-Cookie` forward) —\n * the same shape as every one of demo-site's 8 auth Route Handlers. It is\n * modeled on `apps/demo-site/src/lib/api/proxy.ts` (`proxyToBaseApi`) but is\n * NOT a drop-in replacement for every one of that helper's call sites: it\n * does not support a per-request DYNAMIC `targetPath` (e.g.\n * `api/shop/order/[sessionId]/route.ts`'s `` `/api/site/shop/order/${id}` ``)\n * or an arbitrary extra-`headers` passthrough — both are genuinely out of\n * scope for REB-510 (auth only). demo-site's non-auth proxy routes keep using\n * `proxyToBaseApi` directly; only the 8 auth routes are candidates to adopt\n * this factory.\n *\n * export const POST = createProxyRoute('/api/site/auth/login')\n *\n * Deliberate behavior change vs the original (R6): `proxy.ts` reads\n * `REBLU_API_BASE_URL`/`REBLU_API_KEY` at MODULE LOAD. This factory reads them\n * LAZILY, inside the returned handler, on every request — `opts.baseUrl` /\n * `opts.apiKey` still take priority when provided.\n *\n * Why this factory reads `process.env` at all, unlike `@reblu/site-client`\n * (whose own docblock states \"the SDK never reads `process.env` itself\"):\n * `site-client` is instantiated ONCE by the consuming SITE, which already\n * owns and injects its env at that single call site. `createProxyRoute` is\n * the opposite shape on purpose — a ZERO-CONFIG one-liner meant to be called\n * once per Route Handler FILE (8+ files in a typical SITE), so requiring\n * every file to re-thread `baseUrl`/`apiKey` would defeat the \"one line\"\n * value proposition documented in the README. The env fallback is this\n * factory's deliberate ergonomic default; `opts.baseUrl`/`opts.apiKey` remain\n * available for a SITE that wants explicit, non-`process.env` control.\n *\n * Trust note on header forwarding: `X-Forwarded-For`/`X-Real-IP` are copied\n * from the incoming request VERBATIM (same as the original `proxy.ts`). On\n * Vercel the platform itself sets/overwrites these ahead of the Route\n * Handler, so they're trustworthy there — but this is a distributable SDK a\n * SITE can deploy anywhere. On a host where nothing strips inbound spoofed\n * values, a client can forge the IP base-api rate-limits on. Deploy this\n * factory only behind a platform that normalizes/overwrites these headers,\n * or in front of infra that strips client-supplied ones.\n */\nimport type { NextRequest } from 'next/server'\n\nexport interface CreateProxyRouteOptions {\n /** Overrides `REBLU_API_BASE_URL`. Read lazily per-request when omitted. */\n baseUrl?: string\n /** Overrides `REBLU_API_KEY`. Read lazily per-request when omitted. */\n apiKey?: string\n /** @default 'POST' */\n method?: string\n /** Forward upstream `Set-Cookie` headers to the browser. @default false */\n forwardSetCookie?: boolean\n /**\n * Reject non-`application/json` requests with a Spanish 400 before ever\n * reaching the network — mirrors `magic-link-exchange/route.ts`'s guard.\n * @default false\n */\n requireJson?: boolean\n /**\n * Overrides the Spanish `message` field of the `requireJson` 400 body, for\n * a non-Spanish-default SITE. The `error` code (`invalid_request`) and\n * shape stay fixed — only the human-facing text is overridable.\n * @default 'Solicitud no válida'\n */\n invalidRequestMessage?: string\n /**\n * Hard timeout (ms) for the upstream fetch, enforced via `AbortController`\n * — mirrors `@reblu/site-client`'s own `timeoutMs` (default `8000`, same\n * value). Without this, a hung base-api response would hang the SITE's\n * Route Handler indefinitely instead of degrading.\n * @default 8000\n */\n timeoutMs?: number\n /**\n * Overrides the `message` field of the 500 body returned when neither\n * `opts.baseUrl`/`opts.apiKey` nor `REBLU_API_BASE_URL`/`REBLU_API_KEY`\n * resolve to a non-empty value.\n * @default 'Error de configuración del servidor.'\n */\n configurationErrorMessage?: string\n /**\n * Overrides the `message` field of the 502 body returned when the\n * upstream fetch times out.\n * @default 'La solicitud ha superado el tiempo de espera.'\n */\n timeoutMessage?: string\n /**\n * Overrides the `message` field of the 502 body returned when the\n * upstream fetch fails for any other reason (DNS, connection refused,\n * etc.).\n * @default 'Error de conexión con el servidor.'\n */\n proxyErrorMessage?: string\n /**\n * Transform the raw request body before it is forwarded upstream (REB-837\n * — `createShopCheckoutRoute`'s successPath/cancelPath default-injection\n * is the first caller). Receives the original `request` so a transform\n * can derive values from it (e.g. `request.nextUrl.origin`). Only called\n * for a non-GET/HEAD request with a non-empty body. A thrown error is\n * logged and the ORIGINAL body is forwarded unchanged — a transform bug\n * must never turn into a hard failure for every request through this\n * route.\n */\n transformBody?: (rawBody: string, request: NextRequest) => string\n}\n\nfunction jsonErrorResponse(status: number, error: string, message: string): Response {\n return new Response(\n JSON.stringify({ error, message, correlation_id: crypto.randomUUID() }),\n { status, headers: { 'Content-Type': 'application/json; charset=utf-8' } },\n )\n}\n\n/**\n * Creates a Next.js Route Handler that proxies `request` to\n * `${baseUrl}${targetPath}` at base-api, injecting the `X-Reblu-Api-Key`\n * server-side so it never reaches the browser.\n */\nexport function createProxyRoute(targetPath: string, opts: CreateProxyRouteOptions = {}) {\n const {\n method = 'POST',\n forwardSetCookie = false,\n requireJson = false,\n invalidRequestMessage = 'Solicitud no válida',\n timeoutMs = 8000,\n configurationErrorMessage = 'Error de configuración del servidor.',\n timeoutMessage = 'La solicitud ha superado el tiempo de espera.',\n proxyErrorMessage = 'Error de conexión con el servidor.',\n } = opts\n\n return async function handler(request: NextRequest): Promise<Response> {\n if (requireJson) {\n const contentType = request.headers.get('content-type') ?? ''\n if (!contentType.includes('application/json')) {\n return jsonErrorResponse(400, 'invalid_request', invalidRequestMessage)\n }\n }\n\n // Lazy, per-request env read (R6) — opts win when provided.\n const baseUrl = opts.baseUrl ?? process.env.REBLU_API_BASE_URL ?? ''\n const apiKey = opts.apiKey ?? process.env.REBLU_API_KEY ?? ''\n\n if (!baseUrl || !apiKey) {\n console.error(\n `[createProxyRoute] Missing configuration for ${targetPath}: ` +\n `${!baseUrl ? 'baseUrl/REBLU_API_BASE_URL ' : ''}${!apiKey ? 'apiKey/REBLU_API_KEY' : ''}`.trim(),\n )\n return jsonErrorResponse(500, 'missing_configuration', configurationErrorMessage)\n }\n\n const forwardedHeaders: Record<string, string> = {}\n const ua = request.headers.get('user-agent')\n if (ua) forwardedHeaders['user-agent'] = ua\n const xff = request.headers.get('x-forwarded-for')\n if (xff) forwardedHeaders['x-forwarded-for'] = xff\n const xri = request.headers.get('x-real-ip')\n if (xri) forwardedHeaders['x-real-ip'] = xri\n\n const upperMethod = method.toUpperCase()\n let body =\n upperMethod === 'GET' || upperMethod === 'HEAD' ? undefined : await request.text()\n if (body && opts.transformBody) {\n try {\n body = opts.transformBody(body, request)\n } catch (err) {\n console.error(`[createProxyRoute] transformBody threw for ${targetPath}:`, err)\n }\n }\n // Forward the original query string (e.g. `?slug=x`) regardless of\n // method — `targetPath` is fixed at factory-call time, so without this a\n // GET route configured via `{ method: 'GET' }` would silently drop it.\n const search = request.nextUrl?.search ?? ''\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n\n try {\n const res = await fetch(`${baseUrl}${targetPath}${search}`, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Reblu-Api-Key': apiKey,\n ...forwardedHeaders,\n },\n ...(body ? { body } : {}),\n cache: 'no-store',\n signal: controller.signal,\n })\n\n const data = await res.text()\n const responseHeaders = new Headers({\n 'Content-Type': res.headers.get('Content-Type') ?? 'application/json',\n })\n\n if (forwardSetCookie) {\n for (const cookie of res.headers.getSetCookie()) {\n responseHeaders.append('set-cookie', cookie)\n }\n }\n\n return new Response(data, { status: res.status, headers: responseHeaders })\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n console.error(`[createProxyRoute] Timed out forwarding to ${targetPath} after ${timeoutMs}ms`)\n return jsonErrorResponse(502, 'upstream_timeout', timeoutMessage)\n }\n console.error(`[createProxyRoute] Error forwarding to ${targetPath}:`, err)\n return jsonErrorResponse(502, 'proxy_error', proxyErrorMessage)\n } finally {\n clearTimeout(timer)\n }\n }\n}\n","/**\n * Stripe Checkout redirect-origin guard (REB-836, defence in depth) — the\n * server-side half. `createShopCheckoutRoute` re-checks base-api's Checkout\n * URL before handing it to the browser, which otherwise immediately\n * `window.location.assign`s it.\n *\n * This is a DELIBERATE DUPLICATE of `@reblu/site-client`'s\n * `isSafeCheckoutRedirectUrl` (`packages/site-client/src/checkout-redirect.ts`),\n * not a shared import: the `./shop-route-handlers` entry is dependency-free by\n * design (see `../route-handlers.ts`'s docblock — \"must stay dependency-free\n * (no `@reblu/site-ui`)... so importing it from an `app/api/*` route never\n * drags in client components\"), and pulling in `@reblu/site-client` here\n * would break that isolation. Update both together if Stripe's Checkout host\n * ever changes.\n */\n\n/** The only Checkout redirect host trusted by default. */\nexport const DEFAULT_CHECKOUT_REDIRECT_HOSTS: readonly string[] = ['checkout.stripe.com']\n\n/**\n * True only when `url` parses as an absolute URL, uses `https:`, and its\n * hostname is in `allowedHosts` (default: {@link DEFAULT_CHECKOUT_REDIRECT_HOSTS}).\n * Never throws — an unparsable `url` is simply unsafe.\n */\nexport function isSafeCheckoutRedirectUrl(\n url: string,\n allowedHosts: readonly string[] = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n): boolean {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return false\n }\n return parsed.protocol === 'https:' && allowedHosts.includes(parsed.hostname)\n}\n","/**\n * `@reblu/site-next/shop-route-handlers` (REB-829) — Route Handler factories\n * for the cart/checkout write flow (`/api/site/shop/{checkout, validate-cart,\n * apply-discount, check-email, order/[sessionId]}`). Every demo-site route\n * these replace (`app/api/shop/*`) either hand-rolled the SAME proxy\n * boilerplate `./route-handlers`'s `createProxyRoute` already generalizes\n * (checkout/validate-cart/apply-discount/check-email), or called\n * `proxyToBaseApi` directly for the one dynamic-path route (`order/\n * [sessionId]`) that `createProxyRoute` deliberately does not cover (its own\n * docblock: \"a per-request DYNAMIC targetPath... is genuinely out of scope\").\n * This module is the dynamic-path extension `createProxyRoute` named as a\n * gap, kept as its own file (not folded into `./server`, REB-809's filters/\n * collection glue) because it is a distinct concern — a page that only reads\n * shop data never needs these factories, and a cart/checkout integration\n * never needs the filters glue.\n *\n * Server-only, dependency-free (only `createProxyRoute`, the local\n * `./checkout-redirect` helper, and the `next/server` type) — same isolation\n * direction as `./route-handlers`/`./shop/server.ts`: this entry must NEVER\n * carry `'use client'` (see `tsup.config.ts`).\n *\n * Mirrors `createProxyRoute`'s own API-key/server-only handling verbatim —\n * `REBLU_API_KEY` is read lazily, server-side, and injected as\n * `X-Reblu-Api-Key` on the upstream fetch only; it never reaches the\n * Response these handlers return to the browser.\n *\n * // app/api/shop/checkout/route.ts\n * export const POST = createShopCheckoutRoute()\n *\n * // app/api/shop/order/[sessionId]/route.ts\n * export const GET = createShopOrderRoute()\n */\nimport type { NextRequest } from 'next/server'\nimport { createProxyRoute, type CreateProxyRouteOptions } from '../route-handlers'\nimport { DEFAULT_CHECKOUT_REDIRECT_HOSTS, isSafeCheckoutRedirectUrl } from './checkout-redirect'\n\n/** Options for a fixed-path write route. `method` is always `'POST'` here. */\nexport type ShopWriteRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `requireJson: true` by default for every write route below: the browser\n * clients these proxy for (the cart/checkout UI) always set\n * `Content-Type: application/json`, and rejecting anything else with a 400\n * BEFORE the upstream fetch is strictly safer than forwarding an\n * unparseable body to base-api. A SITE with a non-JSON caller can still pass\n * `{ requireJson: false }`.\n */\nconst DEFAULT_WRITE_OPTS: ShopWriteRouteOptions = { requireJson: true }\n\n/** Options for `createShopCheckoutRoute` — standard success/cancel URLs (REB-837) + the redirect-origin re-check (REB-836). */\nexport interface ShopCheckoutRouteOptions extends ShopWriteRouteOptions {\n /**\n * Path (relative to the resolved SITE origin — see `siteOrigin` below,\n * never a caller-supplied value, so this can never redirect off-SITE)\n * used to build a default `successUrl` when the posted body doesn't\n * already include one. The literal `{CHECKOUT_SESSION_ID}` placeholder is\n * preserved verbatim — Stripe substitutes it at redirect time, same as\n * `cartPath`'s `{cart}` shorthand in the issue brief. Normalized (REB-837\n * review fix): must resolve to a single SITE-relative path starting with\n * `/` — an absolute URL or a protocol-relative `//` is rejected (logged,\n * falls back to the default) rather than silently building an off-SITE\n * redirect.\n * @default `${cartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n */\n successPath?: string\n /**\n * Path used to build a default `cancelUrl`, same normalization and origin\n * rule as `successPath`.\n * @default `${cartPath}?pago=cancelado`\n */\n cancelPath?: string\n /** Base path the `successPath`/`cancelPath` defaults are built from. @default '/cart' */\n cartPath?: string\n /**\n * Explicit SITE origin to build `successUrl`/`cancelUrl` from, overriding\n * `request.nextUrl.origin` (REB-837 review fix). Needed behind a reverse\n * proxy or when the Route Handler's own request origin doesn't match the\n * SITE's real public origin. Falls back to `NEXT_PUBLIC_SITE_URL` when\n * omitted, then to `request.nextUrl.origin`.\n */\n siteOrigin?: string\n /** Allowed hosts for base-api's returned Checkout URL. @default ['checkout.stripe.com'] */\n allowedRedirectHosts?: string[]\n /**\n * Overrides the `message` field of the 502 body returned when base-api's\n * Checkout URL fails the redirect-origin re-check.\n * @default 'Error de conexión con el proveedor de pago.'\n */\n unsafeRedirectMessage?: string\n}\n\n/**\n * Normalizes a SITE-relative path for `successPath`/`cancelPath`/`cartPath`\n * (REB-837 review fix): must start with exactly one `/`, never an absolute\n * URL (`scheme://…`) or a protocol-relative `//…` — either would let a\n * misconfigured caller build a redirect off the SITE's own origin despite\n * the origin itself being trustworthy (request-derived or explicit\n * `siteOrigin`). A path missing its leading slash is PREFIXED with one; an\n * absolute/protocol-relative path is REJECTED outright (logged once) and\n * falls back to `fallback` rather than silently building a dangerous URL.\n */\nfunction normalizeSitePath(path: string, fallback: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path) || path.startsWith('//')) {\n console.error(\n `[createShopCheckoutRoute] Rejected an absolute/protocol-relative path \"${path}\" — using the default instead.`,\n )\n return fallback\n }\n return path.startsWith('/') ? path : `/${path}`\n}\n\nfunction unsafeRedirectResponse(message: string): Response {\n return new Response(\n JSON.stringify({ error: 'unsafe_redirect', message, correlation_id: crypto.randomUUID() }),\n { status: 502, headers: { 'Content-Type': 'application/json; charset=utf-8' } },\n )\n}\n\n/** Extracts `data.url` from a JSON response body, or `undefined` if absent/malformed. */\nfunction extractCheckoutUrl(rawBody: string): string | undefined {\n try {\n const parsed = JSON.parse(rawBody) as { data?: { url?: unknown } }\n return typeof parsed?.data?.url === 'string' ? parsed.data.url : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Transport headers that describe the UPSTREAM byte stream, not `rawBody`\n * (re-serialized text `fetch` already decoded) — forwarding them verbatim\n * would tell the browser to gunzip/measure plain JSON and break checkout.\n * Review follow-up (native review on REB-836): this is an explicit blocklist,\n * not reliance on `createProxyRoute`'s current header set, so a future change\n * to that shared factory (e.g. the parallel REB-837 work) can't silently\n * reintroduce the leak here.\n */\nconst UNSAFE_PASSTHROUGH_HEADERS = new Set(['content-encoding', 'content-length', 'transfer-encoding'])\n\n/** Copies `headers` minus {@link UNSAFE_PASSTHROUGH_HEADERS} — safe to pair with a re-serialized body. */\nfunction safeResponseHeaders(headers: Headers): Headers {\n const safe = new Headers()\n for (const [key, value] of headers.entries()) {\n if (!UNSAFE_PASSTHROUGH_HEADERS.has(key.toLowerCase())) safe.set(key, value)\n }\n return safe\n}\n\n/**\n * `POST /api/site/shop/checkout` — starts a hosted Stripe Checkout session.\n *\n * REB-837: when the posted body doesn't already carry `successUrl`/\n * `cancelUrl` (an explicit value on the body always wins — this never\n * overrides a caller that builds its own), injects them built from\n * `successPath`/`cancelPath` and the resolved SITE origin (`siteOrigin` >\n * `NEXT_PUBLIC_SITE_URL` > `request.nextUrl.origin` — see `siteOrigin`'s\n * docblock). If no origin resolves, the body is forwarded unchanged —\n * base-api's own required-field validation is the backstop, not a second\n * origin check here.\n *\n * REB-836 (defence in depth): every SITE hands the response's `url` straight\n * to `window.location.assign`, so on a successful upstream response this\n * RE-CHECKS that `url` is `https:` on an allowed host (default\n * `checkout.stripe.com`) before ever returning it to the browser — a\n * compromised/misconfigured base-api or a tampered response in transit must\n * never redirect a buyer off Stripe's domain. Mirrors `@reblu/site-client`'s\n * own `shop.checkout()` guard (`./checkout-redirect.ts`'s docblock explains\n * why this is a deliberate duplicate, not a shared import). Non-2xx upstream\n * responses (validation errors, 404s, etc.) pass through unchanged — there is\n * no redirect URL to check.\n */\nexport function createShopCheckoutRoute(opts: ShopCheckoutRouteOptions = {}) {\n const {\n successPath,\n cancelPath,\n cartPath = '/cart',\n siteOrigin,\n allowedRedirectHosts = DEFAULT_CHECKOUT_REDIRECT_HOSTS,\n unsafeRedirectMessage = 'Error de conexión con el proveedor de pago.',\n ...proxyOpts\n } = opts\n\n const resolvedCartPath = normalizeSitePath(cartPath, '/cart')\n const defaultSuccessPath = `${resolvedCartPath}?pago=ok&session_id={CHECKOUT_SESSION_ID}`\n const defaultCancelPath = `${resolvedCartPath}?pago=cancelado`\n const resolvedSuccessPath =\n successPath !== undefined ? normalizeSitePath(successPath, defaultSuccessPath) : defaultSuccessPath\n const resolvedCancelPath =\n cancelPath !== undefined ? normalizeSitePath(cancelPath, defaultCancelPath) : defaultCancelPath\n\n const proxyHandler = createProxyRoute('/api/site/shop/checkout', {\n ...DEFAULT_WRITE_OPTS,\n ...proxyOpts,\n transformBody: (rawBody, request) => {\n // Lazy, per-request (R6): `siteOrigin` wins (explicit, proxy-safe),\n // then `NEXT_PUBLIC_SITE_URL` (env fallback for a SITE behind a\n // reverse proxy where `request.nextUrl.origin` doesn't match its real\n // public origin), then the request's own origin.\n const origin = siteOrigin ?? process.env.NEXT_PUBLIC_SITE_URL ?? request.nextUrl?.origin\n if (!origin) return rawBody\n\n let parsed: Record<string, unknown>\n try {\n parsed = JSON.parse(rawBody) as Record<string, unknown>\n } catch {\n return rawBody\n }\n\n if (typeof parsed.successUrl !== 'string') {\n parsed.successUrl = `${origin}${resolvedSuccessPath}`\n }\n if (typeof parsed.cancelUrl !== 'string') {\n parsed.cancelUrl = `${origin}${resolvedCancelPath}`\n }\n return JSON.stringify(parsed)\n },\n })\n\n return async function handler(request: NextRequest): Promise<Response> {\n const response = await proxyHandler(request)\n if (!response.ok) return response\n\n const rawBody = await response.text()\n const url = extractCheckoutUrl(rawBody)\n if (!url || !isSafeCheckoutRedirectUrl(url, allowedRedirectHosts)) {\n console.error('[createShopCheckoutRoute] Blocked an unsafe checkout redirect URL')\n return unsafeRedirectResponse(unsafeRedirectMessage)\n }\n\n return new Response(rawBody, { status: response.status, headers: safeResponseHeaders(response.headers) })\n }\n}\n\n/** `POST /api/site/shop/validate-cart` — live stock/price re-check before checkout. */\nexport function createShopValidateCartRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/validate-cart', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/apply-discount` — validates a discount code against the cart. */\nexport function createShopApplyDiscountRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/apply-discount', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** `POST /api/site/shop/check-email` — whether a guest's email already has an account. */\nexport function createShopCheckEmailRoute(opts: ShopWriteRouteOptions = {}) {\n return createProxyRoute('/api/site/shop/check-email', { ...DEFAULT_WRITE_OPTS, ...opts })\n}\n\n/** Options for the dynamic order-lookup route. `method` is always `'GET'`. */\nexport type ShopOrderRouteOptions = Omit<CreateProxyRouteOptions, 'method'>\n\n/**\n * `GET /api/site/shop/order/[sessionId]` — the one route in this file with a\n * per-request dynamic target path, so it can't be a one-line\n * `createProxyRoute(...)` call like the others: it reads Next's route\n * `params`, builds `/api/site/shop/order/{sessionId}` per request, and\n * delegates everything else (env, timeout, forwarding, error shaping) to\n * `createProxyRoute`.\n */\nexport function createShopOrderRoute(opts: ShopOrderRouteOptions = {}) {\n return async function handler(\n request: NextRequest,\n context: { params: Promise<{ sessionId: string }> },\n ): Promise<Response> {\n const { sessionId } = await context.params\n const targetPath = `/api/site/shop/order/${encodeURIComponent(sessionId)}`\n return createProxyRoute(targetPath, { ...opts, method: 'GET' })(request)\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- export { a as ShopCollection, S as ShopFilterHrefs, b as buildShopFilterHrefs, g as getShopCollection, p as parseShopSearchParams, s as serializeShopParams, t as toURLSearchParams } from './collection-BE5dhf-r.cjs';
1
+ export { S as ShopCollection, a as ShopFilterHrefs, b as buildShopFilterHrefs, g as getShopCollection, p as parseShopSearchParams, s as serializeShopParams, t as toURLSearchParams } from './collection-gAD_g03I.cjs';
2
2
  import { RebluClient } from '@reblu/site-client';
3
3
  import { ShopOrder } from '@reblu/site-contracts';
4
4
 
@@ -1,4 +1,4 @@
1
- export { a as ShopCollection, S as ShopFilterHrefs, b as buildShopFilterHrefs, g as getShopCollection, p as parseShopSearchParams, s as serializeShopParams, t as toURLSearchParams } from './collection-BE5dhf-r.js';
1
+ export { S as ShopCollection, a as ShopFilterHrefs, b as buildShopFilterHrefs, g as getShopCollection, p as parseShopSearchParams, s as serializeShopParams, t as toURLSearchParams } from './collection-gAD_g03I.js';
2
2
  import { RebluClient } from '@reblu/site-client';
3
3
  import { ShopOrder } from '@reblu/site-contracts';
4
4
 
package/dist/shop.cjs CHANGED
@@ -26,7 +26,9 @@ __export(shop_exports, {
26
26
  parseShopSearchParams: () => parseShopSearchParams,
27
27
  serializeShopParams: () => serializeShopParams,
28
28
  toURLSearchParams: () => toURLSearchParams,
29
- useShopFilterLinks: () => useShopFilterLinks
29
+ useCheckoutReturn: () => useCheckoutReturn,
30
+ useShopFilterLinks: () => useShopFilterLinks,
31
+ useShopOrderStatus: () => useShopOrderStatus
30
32
  });
31
33
  module.exports = __toCommonJS(shop_exports);
32
34
 
@@ -201,6 +203,223 @@ async function getShopCollection(client, params) {
201
203
  }
202
204
  return { products: productsResult.value, facets: facetsResult.value, facetsStatus: "ok" };
203
205
  }
206
+
207
+ // src/shop/use-shop-order-status.ts
208
+ var import_react = require("react");
209
+
210
+ // src/cart/store.ts
211
+ var import_zustand = require("zustand");
212
+ var import_middleware = require("zustand/middleware");
213
+ var CART_STORAGE_KEY = "shop-cart";
214
+ var CART_STORE_PERSIST_VERSION = 0;
215
+ function isSameItem(a, productId, variantId) {
216
+ return a.productId === productId && (a.variantId ?? null) === (variantId ?? null);
217
+ }
218
+ function normalizeQuantity(quantity, maxStock) {
219
+ if (!Number.isFinite(quantity)) return 0;
220
+ const truncated = Math.trunc(quantity);
221
+ if (truncated < 1) return 0;
222
+ if (maxStock === null) return truncated;
223
+ const maxTruncated = Math.trunc(maxStock);
224
+ if (maxTruncated < 1) return 0;
225
+ return Math.min(truncated, maxTruncated);
226
+ }
227
+ function createCartStore() {
228
+ return (0, import_zustand.create)()(
229
+ (0, import_middleware.persist)(
230
+ (set, get) => ({
231
+ items: [],
232
+ addItem: (item) => {
233
+ let outcome = { status: "refused", quantity: 0 };
234
+ set((state) => {
235
+ const existing = state.items.find((i) => isSameItem(i, item.productId, item.variantId));
236
+ if (existing) {
237
+ if (item.maxStock !== null && Math.trunc(item.maxStock) < 1) {
238
+ outcome = { status: "refused", quantity: 0 };
239
+ return {
240
+ items: state.items.filter((i) => !isSameItem(i, item.productId, item.variantId))
241
+ };
242
+ }
243
+ const requestedQty = existing.quantity + item.quantity;
244
+ const newQty = normalizeQuantity(requestedQty, item.maxStock);
245
+ if (newQty < 1) {
246
+ outcome = { status: "refused", quantity: existing.quantity };
247
+ return {};
248
+ }
249
+ outcome = {
250
+ status: newQty <= existing.quantity ? "refused" : newQty < requestedQty ? "clamped" : "added",
251
+ quantity: newQty
252
+ };
253
+ return {
254
+ items: state.items.map(
255
+ (i) => isSameItem(i, item.productId, item.variantId) ? { ...i, quantity: newQty, maxStock: item.maxStock } : i
256
+ )
257
+ };
258
+ }
259
+ const qty = normalizeQuantity(item.quantity, item.maxStock);
260
+ if (qty < 1) {
261
+ outcome = { status: "refused", quantity: 0 };
262
+ return {};
263
+ }
264
+ outcome = { status: qty < item.quantity ? "clamped" : "added", quantity: qty };
265
+ return { items: [...state.items, { ...item, quantity: qty }] };
266
+ });
267
+ return outcome;
268
+ },
269
+ removeItem: (productId, variantId) => {
270
+ set((state) => ({
271
+ items: state.items.filter((i) => !isSameItem(i, productId, variantId))
272
+ }));
273
+ },
274
+ updateQuantity: (productId, variantId, quantity) => {
275
+ const existing = get().items.find((i) => isSameItem(i, productId, variantId));
276
+ if (!existing) return;
277
+ const newQty = normalizeQuantity(quantity, existing.maxStock);
278
+ if (newQty < 1) {
279
+ get().removeItem(productId, variantId);
280
+ return;
281
+ }
282
+ set((state) => ({
283
+ items: state.items.map(
284
+ (i) => isSameItem(i, productId, variantId) ? { ...i, quantity: newQty } : i
285
+ )
286
+ }));
287
+ },
288
+ clearCart: () => set({ items: [] }),
289
+ getTotal: () => get().items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0),
290
+ getItemCount: () => get().items.reduce((sum, item) => sum + item.quantity, 0)
291
+ }),
292
+ {
293
+ name: CART_STORAGE_KEY,
294
+ version: CART_STORE_PERSIST_VERSION,
295
+ skipHydration: true
296
+ }
297
+ )
298
+ );
299
+ }
300
+ var CART_STORE_GLOBAL_KEY = /* @__PURE__ */ Symbol.for(
301
+ `@reblu/site-next/cart-store@v${CART_STORE_PERSIST_VERSION}`
302
+ );
303
+ var globalForCartStore = globalThis;
304
+ var useCartStore = globalForCartStore[CART_STORE_GLOBAL_KEY] ?? (globalForCartStore[CART_STORE_GLOBAL_KEY] = createCartStore());
305
+
306
+ // src/shop/use-shop-order-status.ts
307
+ function defaultBuildUrl(sessionId) {
308
+ return `/api/shop/order/${encodeURIComponent(sessionId)}`;
309
+ }
310
+ function useShopOrderStatus(sessionId, opts = {}) {
311
+ const {
312
+ buildUrl = defaultBuildUrl,
313
+ timeoutMs = 3e4,
314
+ intervalMs = 2e3,
315
+ backoffFactor = 1.5,
316
+ maxIntervalMs = 5e3,
317
+ requestTimeoutMs = 8e3
318
+ } = opts;
319
+ const [status, setStatus] = (0, import_react.useState)(sessionId ? "loading" : "not_found");
320
+ const [order, setOrder] = (0, import_react.useState)(null);
321
+ const [recheckToken, setRecheckToken] = (0, import_react.useState)(0);
322
+ const recheck = (0, import_react.useCallback)(() => setRecheckToken((n) => n + 1), []);
323
+ const buildUrlRef = (0, import_react.useRef)(buildUrl);
324
+ buildUrlRef.current = buildUrl;
325
+ (0, import_react.useEffect)(() => {
326
+ if (!sessionId) {
327
+ setStatus("not_found");
328
+ setOrder(null);
329
+ return;
330
+ }
331
+ setStatus("loading");
332
+ setOrder(null);
333
+ let cancelled = false;
334
+ let pollTimer = null;
335
+ let requestController = null;
336
+ let attempt = 0;
337
+ const finish = (nextStatus) => {
338
+ if (cancelled) return;
339
+ cancelled = true;
340
+ clearTimeout(windowTimer);
341
+ if (pollTimer) clearTimeout(pollTimer);
342
+ requestController?.abort();
343
+ setStatus(nextStatus);
344
+ };
345
+ const windowTimer = setTimeout(() => finish("pending_timeout"), timeoutMs);
346
+ const poll = async () => {
347
+ if (cancelled) return;
348
+ requestController = new AbortController();
349
+ const requestTimer = setTimeout(() => requestController?.abort(), requestTimeoutMs);
350
+ try {
351
+ const res = await fetch(buildUrlRef.current(sessionId), {
352
+ signal: requestController.signal,
353
+ cache: "no-store"
354
+ });
355
+ clearTimeout(requestTimer);
356
+ if (cancelled) return;
357
+ if (res.status === 404) {
358
+ finish("not_found");
359
+ return;
360
+ }
361
+ if (!res.ok) {
362
+ finish("error");
363
+ return;
364
+ }
365
+ const body = await res.json();
366
+ if (cancelled) return;
367
+ const data = body && typeof body === "object" && "data" in body && body.data ? body.data : body;
368
+ setOrder(data);
369
+ switch (data.paymentStatus) {
370
+ case "PAID":
371
+ useCartStore.getState().clearCart();
372
+ finish("paid");
373
+ return;
374
+ case "FAILED":
375
+ finish("failed");
376
+ return;
377
+ case "REFUNDED":
378
+ finish("refunded");
379
+ return;
380
+ case "PARTIALLY_REFUNDED":
381
+ finish("partially_refunded");
382
+ return;
383
+ case "PENDING":
384
+ default: {
385
+ if (cancelled) return;
386
+ const nextDelay = Math.min(intervalMs * backoffFactor ** attempt, maxIntervalMs);
387
+ attempt += 1;
388
+ pollTimer = setTimeout(poll, nextDelay);
389
+ }
390
+ }
391
+ } catch (err) {
392
+ clearTimeout(requestTimer);
393
+ if (cancelled) return;
394
+ if (err instanceof Error && err.name === "AbortError") {
395
+ const nextDelay = Math.min(intervalMs * backoffFactor ** attempt, maxIntervalMs);
396
+ attempt += 1;
397
+ pollTimer = setTimeout(poll, nextDelay);
398
+ return;
399
+ }
400
+ finish("error");
401
+ }
402
+ };
403
+ void poll();
404
+ return () => {
405
+ cancelled = true;
406
+ clearTimeout(windowTimer);
407
+ if (pollTimer) clearTimeout(pollTimer);
408
+ requestController?.abort();
409
+ };
410
+ }, [sessionId, recheckToken, timeoutMs, intervalMs, backoffFactor, maxIntervalMs, requestTimeoutMs]);
411
+ return { status, order, recheck };
412
+ }
413
+
414
+ // src/shop/use-checkout-return.ts
415
+ var import_navigation2 = require("next/navigation");
416
+ function useCheckoutReturn() {
417
+ const searchParams = (0, import_navigation2.useSearchParams)();
418
+ const pago = searchParams.get("pago");
419
+ const sessionId = searchParams.get("session_id");
420
+ const status = pago === "ok" ? "ok" : pago === "cancelado" ? "cancelled" : null;
421
+ return { status, sessionId };
422
+ }
204
423
  // Annotate the CommonJS export names for ESM import in node:
205
424
  0 && (module.exports = {
206
425
  buildShopFilterHrefs,
@@ -208,6 +427,8 @@ async function getShopCollection(client, params) {
208
427
  parseShopSearchParams,
209
428
  serializeShopParams,
210
429
  toURLSearchParams,
211
- useShopFilterLinks
430
+ useCheckoutReturn,
431
+ useShopFilterLinks,
432
+ useShopOrderStatus
212
433
  });
213
434
  //# sourceMappingURL=shop.cjs.map
package/dist/shop.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shop/index.ts","../src/shop/use-shop-filter-links.ts","../src/shop/params.ts","../src/shop/collection.ts"],"sourcesContent":["'use client'\n\n/**\n * `@reblu/site-next/shop` (REB-809) — the client-reactive half of the shop\n * filters glue. This barrel MUST begin with its OWN leading `'use client'`\n * directive: tsup only preserves a directive prologue declared by the entry\n * file itself, not one merely re-exported from a child module (same R4 rule\n * as `./auth`/`./academy`, see `tsup.config.ts`).\n *\n * Re-exports the pure parse/serialize/href helpers too (same dual-export\n * pattern as `./academy` re-exporting `deriveValidationFields`) so a page\n * that is ALREADY a Client Component can import everything from one subpath.\n * A Server Component must import those from `./shop-server` instead — this\n * whole entry, including the pure helpers, looks client-only to Next's RSC\n * boundary once `'use client'` is on it.\n */\nexport { useShopFilterLinks } from './use-shop-filter-links'\nexport {\n parseShopSearchParams,\n serializeShopParams,\n toURLSearchParams,\n buildShopFilterHrefs,\n type ShopFilterHrefs,\n} from './params'\nexport { getShopCollection, type ShopCollection } from './collection'\n","'use client'\n\nimport { usePathname, useSearchParams } from 'next/navigation'\n\nimport { buildShopFilterHrefs, parseShopSearchParams, type ShopFilterHrefs } from './params'\n\n/**\n * Client-reactive version of `buildShopFilterHrefs` (REB-809), bound to the\n * CURRENT URL via `next/navigation`. Use it when a shop collection's filter\n * bar is its own Client Component (e.g. a sticky sidebar) and you want the\n * hrefs to update instantly as the URL changes client-side, without every\n * caller re-deriving `pathname`/`searchParams` by hand.\n *\n * A Server Component does NOT need this hook — it already has `searchParams`\n * as a prop and a static route pathname, so it calls\n * `buildShopFilterHrefs` (from `./params` or the `./shop-server` subpath)\n * directly. Reach for this hook only from inside a `'use client'` boundary.\n */\nexport function useShopFilterLinks(): ShopFilterHrefs {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const current = parseShopSearchParams(searchParams)\n return buildShopFilterHrefs(current, pathname)\n}\n","import type { ShopProductsParams } from '@reblu/site-client'\n\n/**\n * REB-809 — URL ↔ `ShopProductsParams` glue for a SITE's shop/collection page.\n *\n * Mirrors base-api's own `parseListParams`\n * (`apps/base-api/src/lib/shop/catalogue-query.ts`) deliberately: the SAME\n * bracket shape (`taxonomy[<key>]=<slug>`, `attr[<key>]=<value>`, repeatable\n * for `attr`), the SAME MVP collapse for `taxonomy` (one term slug per axis —\n * a repeat keeps only the first), and the SAME defensive caps on bracket\n * key/value counts. Keeping this a mirror rather than a re-implementation\n * means a URL this module builds is EXACTLY the URL the server will parse\n * back to the same params — there is no second, drifting parser.\n */\n\nconst MAX_Q_LEN = 200\nconst MAX_FILTER_KEYS = 30\nconst MAX_VALUE_LEN = 120\nconst MAX_VALUES_PER_KEY = 30\n/**\n * Mirrors `shopProductListParamsSchema`'s `limit: z.number().int().min(1).max(50)`\n * in `apps/base-api/src/lib/contracts/shop.contract.ts` — keep the two in sync;\n * a caller-side limit above this is server-rejected/clamped anyway, but parsing\n * it through here means a URL this module builds never claims a limit the\n * server will refuse.\n */\nconst MAX_LIMIT = 50\n\nconst SHOP_SORTS = ['price_asc', 'price_desc', 'newest', 'on_sale'] as const\n\nfunction isShopSort(value: string): value is (typeof SHOP_SORTS)[number] {\n return (SHOP_SORTS as readonly string[]).includes(value)\n}\n\n/**\n * Parse a non-negative integer, STRICT digits only — `/^\\d+$/`, so '12abc'\n * and '1.5' are rejected outright rather than `Number.parseInt` silently\n * taking the leading '12'/'1' and discarding the rest as if it were valid\n * input. Named for exactly what it accepts: >= 0, not \"positive\" (which\n * excludes 0) — callers that need >= 1 (page, limit) enforce that bound\n * themselves on top of this.\n */\nfunction nonNegativeInt(raw: string | null): number | undefined {\n if (raw === null || !/^\\d+$/.test(raw)) return undefined\n const n = Number.parseInt(raw, 10)\n return Number.isNaN(n) ? undefined : n\n}\n\n/**\n * Copy a (possibly null-prototype) object's own enumerable properties into a\n * fresh PLAIN object — `Object.prototype` in its chain, nothing else.\n *\n * R3-001: this module's internal accumulators use `Object.create(null)` for\n * prototype-pollution safety (see `parseBracketParams`' own doc below), but\n * that safety property must NEVER leak into `ShopProductsParams.taxonomy`/\n * `.attr`, the values a SITE actually receives. React Flight — the\n * serialization React Server Components use to send props across the\n * Server→Client boundary — refuses a non-plain object (`Object.\n * getPrototypeOf(x) !== Object.prototype`) with a hard runtime error. This\n * package's own README tells a Client Component caller to pass\n * `active={params}` to `ShopFilters`' `onChange` mode, so a null-prototype\n * `taxonomy`/`attr` reaching that boundary would crash every SITE that\n * followed the documented pattern. Object spread (`{ ...source }`) always\n * produces a plain object regardless of the source's own prototype, which is\n * exactly the property this needs and `Object.assign({}, source)` also has —\n * spread is used here for clarity.\n */\nfunction toPlainObject<T>(source: Record<string, T>): Record<string, T> {\n return { ...source }\n}\n\n/**\n * Parse `bracket[key]` params into a map, collapsing repeats into arrays.\n *\n * Built on `Object.create(null)` rather than a plain `{}` — a query string is\n * attacker-controlled input, and `taxonomy[constructor]=x` or\n * `taxonomy[toString]=x` against a plain object reads/writes through the\n * INHERITED `Object.prototype` property of that name before this function's\n * own `key in out` / `out[key]` logic ever sees an actual own property,\n * corrupting the key-count bookkeeping and the parsed value for that key. A\n * null-prototype object has no inherited properties at all, so `in` and\n * indexed access only ever see what this loop itself has written — the same\n * defense `JSON.parse` reviewers reach for against prototype-pollution-style\n * bracket keys, applied here to a hand-rolled key/value accumulator.\n *\n * The RETURNED object is converted back to a plain one via\n * {@link toPlainObject} — the null prototype is this function's OWN internal\n * bookkeeping safety, never part of its public output (see that helper's doc\n * for why: React Flight/RSC serialization).\n */\nfunction parseBracketParams(\n searchParams: URLSearchParams,\n prefix: string,\n): Record<string, string | string[]> | undefined {\n const out: Record<string, string | string[]> = Object.create(null)\n const re = new RegExp(`^${prefix}\\\\[(.+)\\\\]$`)\n for (const [rawKey, rawValue] of searchParams.entries()) {\n const match = rawKey.match(re)\n if (!match) continue\n const key = match[1]\n const value = rawValue.trim().slice(0, MAX_VALUE_LEN)\n // REB-827: an empty/whitespace-only value (e.g. a cleared `taxonomy[marca]=`,\n // or a stale/hand-built link) is never a real filter — trim and drop it\n // here, mirroring how `q` is trimmed and dropped when empty, so a key left\n // with no surviving value never enters `out` and never reaches the caller\n // as `{ key: '' }`/`{ key: [] }`.\n if (!value) continue\n if (!(key in out) && Object.keys(out).length >= MAX_FILTER_KEYS) continue\n const existing = out[key]\n if (existing === undefined) out[key] = value\n else if (Array.isArray(existing)) {\n if (existing.length < MAX_VALUES_PER_KEY) existing.push(value)\n } else out[key] = [existing, value]\n }\n return Object.keys(out).length > 0 ? toPlainObject(out) : undefined\n}\n\n/**\n * Parse a `URLSearchParams` (or anything with that shape — `ReadonlyURLSearchParams`\n * from `next/navigation`, or a plain `new URLSearchParams(record)`) into\n * `ShopProductsParams`. Hostile/unknown params are dropped: only the known\n * scalar keys and the two bracket groups are ever read.\n */\nexport function parseShopSearchParams(searchParams: URLSearchParams): ShopProductsParams {\n const params: ShopProductsParams = {}\n\n const page = nonNegativeInt(searchParams.get('page'))\n if (page !== undefined && page >= 1) params.page = page\n\n const limit = nonNegativeInt(searchParams.get('limit'))\n if (limit !== undefined && limit >= 1 && limit <= MAX_LIMIT) params.limit = limit\n\n const q = searchParams.get('q')?.trim().slice(0, MAX_Q_LEN)\n if (q) params.q = q\n\n const sort = searchParams.get('sort')\n if (sort !== null && isShopSort(sort)) params.sort = sort\n\n const category = searchParams.get('category')?.trim()\n if (category) params.category = category\n\n let priceMin = nonNegativeInt(searchParams.get('priceMin'))\n let priceMax = nonNegativeInt(searchParams.get('priceMax'))\n // An inverted range (priceMin > priceMax) would otherwise always match zero\n // products server-side — swap rather than pass through a query that can\n // never return a result, or silently drop half of it.\n if (priceMin !== undefined && priceMax !== undefined && priceMin > priceMax) {\n ;[priceMin, priceMax] = [priceMax, priceMin]\n }\n // Only assign when actually present — an explicit `priceMin: undefined` key\n // is never written, so `toStrictEqual`/`toEqual` against a literal object\n // without that key both hold, and a downstream `'priceMin' in params` check\n // reflects reality.\n if (priceMin !== undefined) params.priceMin = priceMin\n if (priceMax !== undefined) params.priceMax = priceMax\n\n const onSale = searchParams.get('onSale')\n if (onSale === 'true' || onSale === '1') params.onSale = true\n\n const taxonomyRaw = parseBracketParams(searchParams, 'taxonomy')\n if (taxonomyRaw) {\n // A taxonomy axis takes a single term slug in MVP — collapse any repeats,\n // exactly like the server does (see module doc). Object.create(null) for\n // the SAME reason as parseBracketParams' own accumulator: `key` is\n // attacker-controlled (it is the bracket-key from the query string), and\n // a plain `{}` here reintroduces the exact bug parseBracketParams just\n // fixed one step downstream — `taxonomy['__proto__'] = value` on a plain\n // object hits the inherited `__proto__` ACCESSOR, which silently no-ops\n // for a non-object value instead of creating an own property, so that\n // key would vanish from the result entirely (not merely mis-set).\n const taxonomy: Record<string, string> = Object.create(null)\n for (const [key, value] of Object.entries(taxonomyRaw)) {\n taxonomy[key] = Array.isArray(value) ? value[0] : value\n }\n // Plain object out, same reason as parseBracketParams' own return value —\n // see toPlainObject's doc (R3-001, React Flight/RSC serialization).\n params.taxonomy = toPlainObject(taxonomy)\n }\n\n const attr = parseBracketParams(searchParams, 'attr')\n if (attr) params.attr = attr\n\n return params\n}\n\n/**\n * Serialize `ShopProductsParams` into a `URLSearchParams` — the inverse of\n * {@link parseShopSearchParams}. Falsy/empty/undefined fields are omitted, so\n * `parseShopSearchParams(serializeShopParams(params))` round-trips to `params`\n * for any already-canonical (non-empty-string, in-range) input — that is the\n * property the round-trip tests pin.\n */\nexport function serializeShopParams(params: ShopProductsParams): URLSearchParams {\n const sp = new URLSearchParams()\n\n if (params.page !== undefined) sp.set('page', String(params.page))\n if (params.limit !== undefined) sp.set('limit', String(params.limit))\n if (params.q) sp.set('q', params.q)\n if (params.sort) sp.set('sort', params.sort)\n if (params.category) sp.set('category', params.category)\n if (params.priceMin !== undefined) sp.set('priceMin', String(params.priceMin))\n if (params.priceMax !== undefined) sp.set('priceMax', String(params.priceMax))\n if (params.onSale) sp.set('onSale', 'true')\n\n // REB-827: an empty/whitespace-only value is never a real filter — never\n // emit it, matching what parseShopSearchParams would now drop on the way\n // back in anyway (a caller handing this an already-empty `active` value,\n // e.g. a stale object a SITE built itself, must not see it reach the URL).\n //\n // R3-serialize-trim-nullish-throw (review follow-up): `ShopProductsParams`\n // types these values as plain strings, but a JS caller building this object\n // dynamically very commonly ends up with an actual `undefined`/`null` value\n // at runtime regardless of the type (e.g. `taxonomy: { marca: someOptional }`).\n // `typeof value === 'string'` guards `.trim()` itself — a nullish entry is\n // treated exactly like an empty one (never emitted), not a thrown TypeError.\n if (params.taxonomy) {\n for (const [key, value] of Object.entries(params.taxonomy)) {\n if (typeof value === 'string' && value.trim()) sp.set(`taxonomy[${key}]`, value)\n }\n }\n\n if (params.attr) {\n for (const [key, value] of Object.entries(params.attr)) {\n if (Array.isArray(value)) {\n for (const v of value) if (typeof v === 'string' && v.trim()) sp.append(`attr[${key}]`, v)\n } else if (typeof value === 'string' && value.trim()) {\n sp.set(`attr[${key}]`, value)\n }\n }\n }\n\n return sp\n}\n\n/**\n * Convert Next's `searchParams` route prop —\n * `Record<string, string | string[] | undefined>`, already URL-decoded, with\n * a repeated query key grouped into an array — into a `URLSearchParams` for\n * {@link parseShopSearchParams}.\n *\n * NOT the same as `new URLSearchParams(searchParams)`: the native\n * `URLSearchParams` constructor only accepts `Record<string, string>` —\n * handed an array value (`{ 'attr[color]': ['red', 'blue'] }`), it calls\n * `String()` on it, producing ONE entry `attr[color]=red,blue` instead of two\n * repeated `attr[color]=red` / `attr[color]=blue` entries. That silently\n * breaks `parseShopSearchParams`'s repeat-count-based array/scalar\n * distinction (see its own module doc) for every multi-value `attr[<key>]`\n * filter — this helper is what actually preserves the repeat.\n */\nexport function toURLSearchParams(\n searchParams: Record<string, string | string[] | undefined>,\n): URLSearchParams {\n const usp = new URLSearchParams()\n for (const [key, value] of Object.entries(searchParams)) {\n if (value === undefined) continue\n if (Array.isArray(value)) {\n for (const v of value) usp.append(key, v)\n } else {\n usp.append(key, value)\n }\n }\n return usp\n}\n\n/**\n * The navigation seam `@reblu/site-blocks`'s `ShopFilters` calls `hrefs` — kept\n * structurally identical to its `ShopFiltersHrefs` type WITHOUT a package\n * dependency on `@reblu/site-blocks` (this package has none today, and this\n * one shape does not warrant adding one). Any object matching this shape is\n * assignable to `ShopFiltersHrefs` by structural typing.\n */\nexport interface ShopFilterHrefs {\n taxonomy: (axisKey: string, value: string | undefined) => string\n attr: (fieldKey: string, value: string) => string\n price: (min: number | undefined, max: number | undefined) => string\n onSale: (nextActive: boolean) => string\n clearAll: () => string\n}\n\n/**\n * Build `ShopFilters`' href set from the CURRENT params and pathname. Every\n * href preserves every OTHER current param (category, q, sort, every other\n * taxonomy/attr filter) and resets pagination (`page`) — a filter change is\n * never silently stuck on page 4 of a now-smaller result set.\n *\n * Pure and framework-agnostic (no `next/navigation`): a Server Component calls\n * it directly with its own `searchParams` prop and route pathname — no\n * `'use client'` boundary required. See `./use-shop-filter-links` for the\n * client-side hook that wraps this with `usePathname`/`useSearchParams`.\n */\nexport function buildShopFilterHrefs(\n current: ShopProductsParams,\n pathname: string,\n): ShopFilterHrefs {\n function hrefFor(patch: Partial<ShopProductsParams>): string {\n // Any filter change resets pagination — the previous page number almost\n // never still makes sense against the new (usually smaller) result set.\n const next: ShopProductsParams = { ...current, ...patch, page: undefined }\n const qs = serializeShopParams(next).toString()\n return qs ? `${pathname}?${qs}` : pathname\n }\n\n return {\n taxonomy: (axisKey, value) => {\n const taxonomy = { ...current.taxonomy }\n if (value === undefined) delete taxonomy[axisKey]\n else taxonomy[axisKey] = value\n return hrefFor({ taxonomy })\n },\n attr: (fieldKey, value) => {\n const existing = current.attr?.[fieldKey]\n const values = existing === undefined ? [] : Array.isArray(existing) ? existing : [existing]\n const next = values.includes(value) ? values.filter((v) => v !== value) : [...values, value]\n const attr = { ...current.attr }\n if (next.length === 0) delete attr[fieldKey]\n else attr[fieldKey] = next\n return hrefFor({ attr })\n },\n price: (min, max) => hrefFor({ priceMin: min, priceMax: max }),\n onSale: (nextActive) => hrefFor({ onSale: nextActive || undefined }),\n // Clears every FACET dimension only — q/category/sort (the non-facet part\n // of the query) are deliberately preserved, mirroring ShopFilters' own\n // onChange clear-all patch.\n clearAll: () =>\n hrefFor({ taxonomy: undefined, attr: undefined, priceMin: undefined, priceMax: undefined, onSale: undefined }),\n }\n}\n","import type { RebluClient, ListResult } from '@reblu/site-client'\nimport type { ShopFacets, ShopProductListItem } from '@reblu/site-contracts'\nimport type { ShopProductsParams } from '@reblu/site-client'\n\n/**\n * `getShopCollection` (REB-809) — fetches the product list AND its facets with\n * the EXACT SAME params in one call. This is the seam that keeps a shop\n * collection page correct: facet counts must reflect the same filter context\n * the product list is showing, and a hand-rolled page that calls\n * `client.shop.products(params)` and `client.shop.facets(someOtherParams)`\n * separately is exactly how they drift apart.\n *\n * Server-safe (no `'use client'`, no React) — call it directly from an async\n * Server Component (route `page.tsx`) with the params parsed via\n * {@link parseShopSearchParams} from that page's `searchParams` prop.\n */\nexport interface ShopCollection {\n products: ListResult<ShopProductListItem>\n facets: ShopFacets\n /**\n * R3-002: `'ok'` when `client.shop.facets(...)` actually succeeded;\n * `'unavailable'` when it failed and `facets` above is the degraded\n * {@link EMPTY_FACETS} fallback. NOT silent — check this before treating an\n * empty `facets` as \"this tenant configured no filterable axes\" when it may\n * really mean \"the facets endpoint is down right now\". A logged\n * `console.warn` (see below) covers the case nobody checks this field.\n */\n facetsStatus: 'ok' | 'unavailable'\n}\n\n/**\n * Returned when `client.shop.facets(...)` fails — a `ShopFilters` that\n * receives this simply renders nothing (`shouldRenderShopFilters` is false\n * for it with no active selection), which is the correct degraded state: no\n * filter UI, rather than no page at all. Paired with `facetsStatus:\n * 'unavailable'` on the returned {@link ShopCollection}, so a caller that\n * DOES care can tell \"no axes configured\" apart from \"facets endpoint down\".\n */\nconst EMPTY_FACETS: ShopFacets = {\n taxonomies: [],\n attributes: [],\n price: { min: 0, max: 0 },\n onSale: { count: 0 },\n}\n\n/**\n * R4-facets-failure-takes-down-listing: `products` and `facets` are NOT\n * equally critical. A product listing with no products is not a listing —\n * that failure must propagate so the caller's own error handling (or a\n * thrown-away `try/catch` degrading the whole page) sees it. A product\n * listing with no FACETS is still a perfectly good listing, just without\n * filter UI — degrading `facets` to {@link EMPTY_FACETS} on failure, instead\n * of letting one upstream endpoint's outage take the other down too, is what\n * `Promise.allSettled` (rather than `Promise.all`) buys here.\n *\n * R3-002: that degradation must not be SILENT. `console.warn`, bracket-tagged\n * `[getShopCollection]` — the same convention `@reblu/site-next` already uses\n * elsewhere (`route-handlers.ts`'s `[createProxyRoute]`,\n * `set-password.tsx`'s `[SetPassword]`) — fires once per failed call, and the\n * returned `facetsStatus: 'unavailable'` lets a caller that wants to act on\n * it (retry, surface a banner, alert) do so without parsing a log line.\n */\nexport async function getShopCollection(\n client: RebluClient,\n params?: ShopProductsParams,\n): Promise<ShopCollection> {\n const [productsResult, facetsResult] = await Promise.allSettled([\n client.shop.products(params),\n client.shop.facets(params),\n ])\n\n if (productsResult.status === 'rejected') {\n throw productsResult.reason\n }\n\n if (facetsResult.status === 'rejected') {\n console.warn('[getShopCollection] facets fetch failed, degrading to empty ShopFacets:', facetsResult.reason)\n return { products: productsResult.value, facets: EMPTY_FACETS, facetsStatus: 'unavailable' }\n }\n\n return { products: productsResult.value, facets: facetsResult.value, facetsStatus: 'ok' }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/shop/index.ts","../src/shop/use-shop-filter-links.ts","../src/shop/params.ts","../src/shop/collection.ts","../src/shop/use-shop-order-status.ts","../src/cart/store.ts","../src/shop/use-checkout-return.ts"],"sourcesContent":["'use client'\n\n/**\n * `@reblu/site-next/shop` (REB-809) — the client-reactive half of the shop\n * filters glue. This barrel MUST begin with its OWN leading `'use client'`\n * directive: tsup only preserves a directive prologue declared by the entry\n * file itself, not one merely re-exported from a child module (same R4 rule\n * as `./auth`/`./academy`, see `tsup.config.ts`).\n *\n * Re-exports the pure parse/serialize/href helpers too (same dual-export\n * pattern as `./academy` re-exporting `deriveValidationFields`) so a page\n * that is ALREADY a Client Component can import everything from one subpath.\n * A Server Component must import those from `./shop-server` instead — this\n * whole entry, including the pure helpers, looks client-only to Next's RSC\n * boundary once `'use client'` is on it.\n */\nexport { useShopFilterLinks } from './use-shop-filter-links'\nexport {\n parseShopSearchParams,\n serializeShopParams,\n toURLSearchParams,\n buildShopFilterHrefs,\n type ShopFilterHrefs,\n} from './params'\nexport { getShopCollection, type ShopCollection } from './collection'\nexport {\n useShopOrderStatus,\n type ShopOrderStatusState,\n type UseShopOrderStatusOptions,\n type UseShopOrderStatusResult,\n} from './use-shop-order-status'\nexport {\n useCheckoutReturn,\n type CheckoutReturnStatus,\n type UseCheckoutReturnResult,\n} from './use-checkout-return'\n","'use client'\n\nimport { usePathname, useSearchParams } from 'next/navigation'\n\nimport { buildShopFilterHrefs, parseShopSearchParams, type ShopFilterHrefs } from './params'\n\n/**\n * Client-reactive version of `buildShopFilterHrefs` (REB-809), bound to the\n * CURRENT URL via `next/navigation`. Use it when a shop collection's filter\n * bar is its own Client Component (e.g. a sticky sidebar) and you want the\n * hrefs to update instantly as the URL changes client-side, without every\n * caller re-deriving `pathname`/`searchParams` by hand.\n *\n * A Server Component does NOT need this hook — it already has `searchParams`\n * as a prop and a static route pathname, so it calls\n * `buildShopFilterHrefs` (from `./params` or the `./shop-server` subpath)\n * directly. Reach for this hook only from inside a `'use client'` boundary.\n */\nexport function useShopFilterLinks(): ShopFilterHrefs {\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const current = parseShopSearchParams(searchParams)\n return buildShopFilterHrefs(current, pathname)\n}\n","import type { ShopProductsParams } from '@reblu/site-client'\n\n/**\n * REB-809 — URL ↔ `ShopProductsParams` glue for a SITE's shop/collection page.\n *\n * Mirrors base-api's own `parseListParams`\n * (`apps/base-api/src/lib/shop/catalogue-query.ts`) deliberately: the SAME\n * bracket shape (`taxonomy[<key>]=<slug>`, `attr[<key>]=<value>`, repeatable\n * for `attr`), the SAME MVP collapse for `taxonomy` (one term slug per axis —\n * a repeat keeps only the first), and the SAME defensive caps on bracket\n * key/value counts. Keeping this a mirror rather than a re-implementation\n * means a URL this module builds is EXACTLY the URL the server will parse\n * back to the same params — there is no second, drifting parser.\n */\n\nconst MAX_Q_LEN = 200\nconst MAX_FILTER_KEYS = 30\nconst MAX_VALUE_LEN = 120\nconst MAX_VALUES_PER_KEY = 30\n/**\n * Mirrors `shopProductListParamsSchema`'s `limit: z.number().int().min(1).max(50)`\n * in `apps/base-api/src/lib/contracts/shop.contract.ts` — keep the two in sync;\n * a caller-side limit above this is server-rejected/clamped anyway, but parsing\n * it through here means a URL this module builds never claims a limit the\n * server will refuse.\n */\nconst MAX_LIMIT = 50\n\nconst SHOP_SORTS = ['price_asc', 'price_desc', 'newest', 'on_sale'] as const\n\nfunction isShopSort(value: string): value is (typeof SHOP_SORTS)[number] {\n return (SHOP_SORTS as readonly string[]).includes(value)\n}\n\n/**\n * Parse a non-negative integer, STRICT digits only — `/^\\d+$/`, so '12abc'\n * and '1.5' are rejected outright rather than `Number.parseInt` silently\n * taking the leading '12'/'1' and discarding the rest as if it were valid\n * input. Named for exactly what it accepts: >= 0, not \"positive\" (which\n * excludes 0) — callers that need >= 1 (page, limit) enforce that bound\n * themselves on top of this.\n */\nfunction nonNegativeInt(raw: string | null): number | undefined {\n if (raw === null || !/^\\d+$/.test(raw)) return undefined\n const n = Number.parseInt(raw, 10)\n return Number.isNaN(n) ? undefined : n\n}\n\n/**\n * Copy a (possibly null-prototype) object's own enumerable properties into a\n * fresh PLAIN object — `Object.prototype` in its chain, nothing else.\n *\n * R3-001: this module's internal accumulators use `Object.create(null)` for\n * prototype-pollution safety (see `parseBracketParams`' own doc below), but\n * that safety property must NEVER leak into `ShopProductsParams.taxonomy`/\n * `.attr`, the values a SITE actually receives. React Flight — the\n * serialization React Server Components use to send props across the\n * Server→Client boundary — refuses a non-plain object (`Object.\n * getPrototypeOf(x) !== Object.prototype`) with a hard runtime error. This\n * package's own README tells a Client Component caller to pass\n * `active={params}` to `ShopFilters`' `onChange` mode, so a null-prototype\n * `taxonomy`/`attr` reaching that boundary would crash every SITE that\n * followed the documented pattern. Object spread (`{ ...source }`) always\n * produces a plain object regardless of the source's own prototype, which is\n * exactly the property this needs and `Object.assign({}, source)` also has —\n * spread is used here for clarity.\n */\nfunction toPlainObject<T>(source: Record<string, T>): Record<string, T> {\n return { ...source }\n}\n\n/**\n * Parse `bracket[key]` params into a map, collapsing repeats into arrays.\n *\n * Built on `Object.create(null)` rather than a plain `{}` — a query string is\n * attacker-controlled input, and `taxonomy[constructor]=x` or\n * `taxonomy[toString]=x` against a plain object reads/writes through the\n * INHERITED `Object.prototype` property of that name before this function's\n * own `key in out` / `out[key]` logic ever sees an actual own property,\n * corrupting the key-count bookkeeping and the parsed value for that key. A\n * null-prototype object has no inherited properties at all, so `in` and\n * indexed access only ever see what this loop itself has written — the same\n * defense `JSON.parse` reviewers reach for against prototype-pollution-style\n * bracket keys, applied here to a hand-rolled key/value accumulator.\n *\n * The RETURNED object is converted back to a plain one via\n * {@link toPlainObject} — the null prototype is this function's OWN internal\n * bookkeeping safety, never part of its public output (see that helper's doc\n * for why: React Flight/RSC serialization).\n */\nfunction parseBracketParams(\n searchParams: URLSearchParams,\n prefix: string,\n): Record<string, string | string[]> | undefined {\n const out: Record<string, string | string[]> = Object.create(null)\n const re = new RegExp(`^${prefix}\\\\[(.+)\\\\]$`)\n for (const [rawKey, rawValue] of searchParams.entries()) {\n const match = rawKey.match(re)\n if (!match) continue\n const key = match[1]\n const value = rawValue.trim().slice(0, MAX_VALUE_LEN)\n // REB-827: an empty/whitespace-only value (e.g. a cleared `taxonomy[marca]=`,\n // or a stale/hand-built link) is never a real filter — trim and drop it\n // here, mirroring how `q` is trimmed and dropped when empty, so a key left\n // with no surviving value never enters `out` and never reaches the caller\n // as `{ key: '' }`/`{ key: [] }`.\n if (!value) continue\n if (!(key in out) && Object.keys(out).length >= MAX_FILTER_KEYS) continue\n const existing = out[key]\n if (existing === undefined) out[key] = value\n else if (Array.isArray(existing)) {\n if (existing.length < MAX_VALUES_PER_KEY) existing.push(value)\n } else out[key] = [existing, value]\n }\n return Object.keys(out).length > 0 ? toPlainObject(out) : undefined\n}\n\n/**\n * Parse a `URLSearchParams` (or anything with that shape — `ReadonlyURLSearchParams`\n * from `next/navigation`, or a plain `new URLSearchParams(record)`) into\n * `ShopProductsParams`. Hostile/unknown params are dropped: only the known\n * scalar keys and the two bracket groups are ever read.\n */\nexport function parseShopSearchParams(searchParams: URLSearchParams): ShopProductsParams {\n const params: ShopProductsParams = {}\n\n const page = nonNegativeInt(searchParams.get('page'))\n if (page !== undefined && page >= 1) params.page = page\n\n const limit = nonNegativeInt(searchParams.get('limit'))\n if (limit !== undefined && limit >= 1 && limit <= MAX_LIMIT) params.limit = limit\n\n const q = searchParams.get('q')?.trim().slice(0, MAX_Q_LEN)\n if (q) params.q = q\n\n const sort = searchParams.get('sort')\n if (sort !== null && isShopSort(sort)) params.sort = sort\n\n const category = searchParams.get('category')?.trim()\n if (category) params.category = category\n\n let priceMin = nonNegativeInt(searchParams.get('priceMin'))\n let priceMax = nonNegativeInt(searchParams.get('priceMax'))\n // An inverted range (priceMin > priceMax) would otherwise always match zero\n // products server-side — swap rather than pass through a query that can\n // never return a result, or silently drop half of it.\n if (priceMin !== undefined && priceMax !== undefined && priceMin > priceMax) {\n ;[priceMin, priceMax] = [priceMax, priceMin]\n }\n // Only assign when actually present — an explicit `priceMin: undefined` key\n // is never written, so `toStrictEqual`/`toEqual` against a literal object\n // without that key both hold, and a downstream `'priceMin' in params` check\n // reflects reality.\n if (priceMin !== undefined) params.priceMin = priceMin\n if (priceMax !== undefined) params.priceMax = priceMax\n\n const onSale = searchParams.get('onSale')\n if (onSale === 'true' || onSale === '1') params.onSale = true\n\n const taxonomyRaw = parseBracketParams(searchParams, 'taxonomy')\n if (taxonomyRaw) {\n // A taxonomy axis takes a single term slug in MVP — collapse any repeats,\n // exactly like the server does (see module doc). Object.create(null) for\n // the SAME reason as parseBracketParams' own accumulator: `key` is\n // attacker-controlled (it is the bracket-key from the query string), and\n // a plain `{}` here reintroduces the exact bug parseBracketParams just\n // fixed one step downstream — `taxonomy['__proto__'] = value` on a plain\n // object hits the inherited `__proto__` ACCESSOR, which silently no-ops\n // for a non-object value instead of creating an own property, so that\n // key would vanish from the result entirely (not merely mis-set).\n const taxonomy: Record<string, string> = Object.create(null)\n for (const [key, value] of Object.entries(taxonomyRaw)) {\n taxonomy[key] = Array.isArray(value) ? value[0] : value\n }\n // Plain object out, same reason as parseBracketParams' own return value —\n // see toPlainObject's doc (R3-001, React Flight/RSC serialization).\n params.taxonomy = toPlainObject(taxonomy)\n }\n\n const attr = parseBracketParams(searchParams, 'attr')\n if (attr) params.attr = attr\n\n return params\n}\n\n/**\n * Serialize `ShopProductsParams` into a `URLSearchParams` — the inverse of\n * {@link parseShopSearchParams}. Falsy/empty/undefined fields are omitted, so\n * `parseShopSearchParams(serializeShopParams(params))` round-trips to `params`\n * for any already-canonical (non-empty-string, in-range) input — that is the\n * property the round-trip tests pin.\n */\nexport function serializeShopParams(params: ShopProductsParams): URLSearchParams {\n const sp = new URLSearchParams()\n\n if (params.page !== undefined) sp.set('page', String(params.page))\n if (params.limit !== undefined) sp.set('limit', String(params.limit))\n if (params.q) sp.set('q', params.q)\n if (params.sort) sp.set('sort', params.sort)\n if (params.category) sp.set('category', params.category)\n if (params.priceMin !== undefined) sp.set('priceMin', String(params.priceMin))\n if (params.priceMax !== undefined) sp.set('priceMax', String(params.priceMax))\n if (params.onSale) sp.set('onSale', 'true')\n\n // REB-827: an empty/whitespace-only value is never a real filter — never\n // emit it, matching what parseShopSearchParams would now drop on the way\n // back in anyway (a caller handing this an already-empty `active` value,\n // e.g. a stale object a SITE built itself, must not see it reach the URL).\n //\n // R3-serialize-trim-nullish-throw (review follow-up): `ShopProductsParams`\n // types these values as plain strings, but a JS caller building this object\n // dynamically very commonly ends up with an actual `undefined`/`null` value\n // at runtime regardless of the type (e.g. `taxonomy: { marca: someOptional }`).\n // `typeof value === 'string'` guards `.trim()` itself — a nullish entry is\n // treated exactly like an empty one (never emitted), not a thrown TypeError.\n if (params.taxonomy) {\n for (const [key, value] of Object.entries(params.taxonomy)) {\n if (typeof value === 'string' && value.trim()) sp.set(`taxonomy[${key}]`, value)\n }\n }\n\n if (params.attr) {\n for (const [key, value] of Object.entries(params.attr)) {\n if (Array.isArray(value)) {\n for (const v of value) if (typeof v === 'string' && v.trim()) sp.append(`attr[${key}]`, v)\n } else if (typeof value === 'string' && value.trim()) {\n sp.set(`attr[${key}]`, value)\n }\n }\n }\n\n return sp\n}\n\n/**\n * Convert Next's `searchParams` route prop —\n * `Record<string, string | string[] | undefined>`, already URL-decoded, with\n * a repeated query key grouped into an array — into a `URLSearchParams` for\n * {@link parseShopSearchParams}.\n *\n * NOT the same as `new URLSearchParams(searchParams)`: the native\n * `URLSearchParams` constructor only accepts `Record<string, string>` —\n * handed an array value (`{ 'attr[color]': ['red', 'blue'] }`), it calls\n * `String()` on it, producing ONE entry `attr[color]=red,blue` instead of two\n * repeated `attr[color]=red` / `attr[color]=blue` entries. That silently\n * breaks `parseShopSearchParams`'s repeat-count-based array/scalar\n * distinction (see its own module doc) for every multi-value `attr[<key>]`\n * filter — this helper is what actually preserves the repeat.\n */\nexport function toURLSearchParams(\n searchParams: Record<string, string | string[] | undefined>,\n): URLSearchParams {\n const usp = new URLSearchParams()\n for (const [key, value] of Object.entries(searchParams)) {\n if (value === undefined) continue\n if (Array.isArray(value)) {\n for (const v of value) usp.append(key, v)\n } else {\n usp.append(key, value)\n }\n }\n return usp\n}\n\n/**\n * The navigation seam `@reblu/site-blocks`'s `ShopFilters` calls `hrefs` — kept\n * structurally identical to its `ShopFiltersHrefs` type WITHOUT a package\n * dependency on `@reblu/site-blocks` (this package has none today, and this\n * one shape does not warrant adding one). Any object matching this shape is\n * assignable to `ShopFiltersHrefs` by structural typing.\n */\nexport interface ShopFilterHrefs {\n taxonomy: (axisKey: string, value: string | undefined) => string\n attr: (fieldKey: string, value: string) => string\n price: (min: number | undefined, max: number | undefined) => string\n onSale: (nextActive: boolean) => string\n clearAll: () => string\n}\n\n/**\n * Build `ShopFilters`' href set from the CURRENT params and pathname. Every\n * href preserves every OTHER current param (category, q, sort, every other\n * taxonomy/attr filter) and resets pagination (`page`) — a filter change is\n * never silently stuck on page 4 of a now-smaller result set.\n *\n * Pure and framework-agnostic (no `next/navigation`): a Server Component calls\n * it directly with its own `searchParams` prop and route pathname — no\n * `'use client'` boundary required. See `./use-shop-filter-links` for the\n * client-side hook that wraps this with `usePathname`/`useSearchParams`.\n */\nexport function buildShopFilterHrefs(\n current: ShopProductsParams,\n pathname: string,\n): ShopFilterHrefs {\n function hrefFor(patch: Partial<ShopProductsParams>): string {\n // Any filter change resets pagination — the previous page number almost\n // never still makes sense against the new (usually smaller) result set.\n const next: ShopProductsParams = { ...current, ...patch, page: undefined }\n const qs = serializeShopParams(next).toString()\n return qs ? `${pathname}?${qs}` : pathname\n }\n\n return {\n taxonomy: (axisKey, value) => {\n const taxonomy = { ...current.taxonomy }\n if (value === undefined) delete taxonomy[axisKey]\n else taxonomy[axisKey] = value\n return hrefFor({ taxonomy })\n },\n attr: (fieldKey, value) => {\n const existing = current.attr?.[fieldKey]\n const values = existing === undefined ? [] : Array.isArray(existing) ? existing : [existing]\n const next = values.includes(value) ? values.filter((v) => v !== value) : [...values, value]\n const attr = { ...current.attr }\n if (next.length === 0) delete attr[fieldKey]\n else attr[fieldKey] = next\n return hrefFor({ attr })\n },\n price: (min, max) => hrefFor({ priceMin: min, priceMax: max }),\n onSale: (nextActive) => hrefFor({ onSale: nextActive || undefined }),\n // Clears every FACET dimension only — q/category/sort (the non-facet part\n // of the query) are deliberately preserved, mirroring ShopFilters' own\n // onChange clear-all patch.\n clearAll: () =>\n hrefFor({ taxonomy: undefined, attr: undefined, priceMin: undefined, priceMax: undefined, onSale: undefined }),\n }\n}\n","import type { RebluClient, ListResult } from '@reblu/site-client'\nimport type { ShopFacets, ShopProductListItem } from '@reblu/site-contracts'\nimport type { ShopProductsParams } from '@reblu/site-client'\n\n/**\n * `getShopCollection` (REB-809) — fetches the product list AND its facets with\n * the EXACT SAME params in one call. This is the seam that keeps a shop\n * collection page correct: facet counts must reflect the same filter context\n * the product list is showing, and a hand-rolled page that calls\n * `client.shop.products(params)` and `client.shop.facets(someOtherParams)`\n * separately is exactly how they drift apart.\n *\n * Server-safe (no `'use client'`, no React) — call it directly from an async\n * Server Component (route `page.tsx`) with the params parsed via\n * {@link parseShopSearchParams} from that page's `searchParams` prop.\n */\nexport interface ShopCollection {\n products: ListResult<ShopProductListItem>\n facets: ShopFacets\n /**\n * R3-002: `'ok'` when `client.shop.facets(...)` actually succeeded;\n * `'unavailable'` when it failed and `facets` above is the degraded\n * {@link EMPTY_FACETS} fallback. NOT silent — check this before treating an\n * empty `facets` as \"this tenant configured no filterable axes\" when it may\n * really mean \"the facets endpoint is down right now\". A logged\n * `console.warn` (see below) covers the case nobody checks this field.\n */\n facetsStatus: 'ok' | 'unavailable'\n}\n\n/**\n * Returned when `client.shop.facets(...)` fails — a `ShopFilters` that\n * receives this simply renders nothing (`shouldRenderShopFilters` is false\n * for it with no active selection), which is the correct degraded state: no\n * filter UI, rather than no page at all. Paired with `facetsStatus:\n * 'unavailable'` on the returned {@link ShopCollection}, so a caller that\n * DOES care can tell \"no axes configured\" apart from \"facets endpoint down\".\n */\nconst EMPTY_FACETS: ShopFacets = {\n taxonomies: [],\n attributes: [],\n price: { min: 0, max: 0 },\n onSale: { count: 0 },\n}\n\n/**\n * R4-facets-failure-takes-down-listing: `products` and `facets` are NOT\n * equally critical. A product listing with no products is not a listing —\n * that failure must propagate so the caller's own error handling (or a\n * thrown-away `try/catch` degrading the whole page) sees it. A product\n * listing with no FACETS is still a perfectly good listing, just without\n * filter UI — degrading `facets` to {@link EMPTY_FACETS} on failure, instead\n * of letting one upstream endpoint's outage take the other down too, is what\n * `Promise.allSettled` (rather than `Promise.all`) buys here.\n *\n * R3-002: that degradation must not be SILENT. `console.warn`, bracket-tagged\n * `[getShopCollection]` — the same convention `@reblu/site-next` already uses\n * elsewhere (`route-handlers.ts`'s `[createProxyRoute]`,\n * `set-password.tsx`'s `[SetPassword]`) — fires once per failed call, and the\n * returned `facetsStatus: 'unavailable'` lets a caller that wants to act on\n * it (retry, surface a banner, alert) do so without parsing a log line.\n */\nexport async function getShopCollection(\n client: RebluClient,\n params?: ShopProductsParams,\n): Promise<ShopCollection> {\n const [productsResult, facetsResult] = await Promise.allSettled([\n client.shop.products(params),\n client.shop.facets(params),\n ])\n\n if (productsResult.status === 'rejected') {\n throw productsResult.reason\n }\n\n if (facetsResult.status === 'rejected') {\n console.warn('[getShopCollection] facets fetch failed, degrading to empty ShopFacets:', facetsResult.reason)\n return { products: productsResult.value, facets: EMPTY_FACETS, facetsStatus: 'unavailable' }\n }\n\n return { products: productsResult.value, facets: facetsResult.value, facetsStatus: 'ok' }\n}\n","'use client'\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport type { ShopOrder } from '@reblu/site-contracts'\n\nimport { useCartStore } from '../cart/store'\n\nexport type ShopOrderStatusState =\n | 'loading'\n | 'paid'\n | 'pending_timeout'\n | 'failed'\n | 'refunded'\n | 'partially_refunded'\n | 'not_found'\n | 'error'\n\nexport interface UseShopOrderStatusOptions {\n /**\n * Builds the SITE's own order Route Handler path from the session id.\n * @default (id) => `/api/shop/order/${encodeURIComponent(id)}`\n */\n buildUrl?: (sessionId: string) => string\n /**\n * Total polling window before giving up as `pending_timeout`, ms.\n * @default 30000\n *\n * Enforced by its OWN independent timer (REB-837 review fix), not by\n * checking elapsed time inside `poll()` — a hung request that never\n * settles used to block `poll()` from ever reaching that check at all,\n * leaving `status` stuck on `'loading'` forever.\n */\n timeoutMs?: number\n /** Initial delay between polls, ms, before backoff. @default 2000 */\n intervalMs?: number\n /** Multiplier applied to the interval after each PENDING poll. @default 1.5 */\n backoffFactor?: number\n /** Ceiling on the backed-off interval, ms. @default 5000 */\n maxIntervalMs?: number\n /**\n * Hard timeout for a SINGLE request, ms (REB-837 review fix) — aborts and\n * retries with backoff rather than leaving a hung `fetch` blocking\n * indefinitely. Distinct from `timeoutMs` (the WHOLE polling window):\n * one slow request past `requestTimeoutMs` is transient and retried; the\n * window elapsing is terminal (`pending_timeout`).\n * @default 8000\n */\n requestTimeoutMs?: number\n}\n\nexport interface UseShopOrderStatusResult {\n status: ShopOrderStatusState\n order: ShopOrder | null\n /** Re-runs the poll loop from scratch — e.g. the \"Volver a comprobar\" button. */\n recheck: () => void\n}\n\nfunction defaultBuildUrl(sessionId: string): string {\n return `/api/shop/order/${encodeURIComponent(sessionId)}`\n}\n\n/**\n * `useShopOrderStatus` (REB-837) — the client-side half of the order-\n * confirmation flow: polls the SITE's OWN order Route Handler (built from\n * `createShopOrderRoute`, never base-api directly — same rule as every other\n * client-side shop call in this package) for up to `timeoutMs` with backoff,\n * and clears the cart ONLY once the order resolves to `PAID` — never on\n * `pending_timeout`, `failed`, or any other state, so an interrupted/failed\n * payment leaves the visitor's cart exactly as they left it.\n *\n * Checkout is card-only today, so `PENDING` past the window is only ever\n * transient (the confirming webhook landing late) — see REB-836 for the\n * redirect-origin hardening this flow sits behind.\n */\nexport function useShopOrderStatus(\n sessionId: string | null | undefined,\n opts: UseShopOrderStatusOptions = {},\n): UseShopOrderStatusResult {\n const {\n buildUrl = defaultBuildUrl,\n timeoutMs = 30000,\n intervalMs = 2000,\n backoffFactor = 1.5,\n maxIntervalMs = 5000,\n requestTimeoutMs = 8000,\n } = opts\n\n const [status, setStatus] = useState<ShopOrderStatusState>(sessionId ? 'loading' : 'not_found')\n const [order, setOrder] = useState<ShopOrder | null>(null)\n const [recheckToken, setRecheckToken] = useState(0)\n\n const recheck = useCallback(() => setRecheckToken((n) => n + 1), [])\n\n /**\n * \"Latest ref\" for `buildUrl` (REB-837 mutation find): a caller passing an\n * inline `{ buildUrl: (id) => ... }` literal — the natural way to call\n * this hook — gets a NEW function identity every render. Putting that\n * function directly in the polling effect's dependency array caused an\n * infinite loop: `setOrder(freshlyParsedJson)` always produces a new\n * object reference regardless of content, so it forces a re-render even\n * when the status is already settled; the re-render recreates the inline\n * `buildUrl`; the changed dependency re-fires the effect; which fetches\n * and calls `setOrder` again — forever, reproduced as an OOM crash under\n * test. `buildUrl` is read through this ref inside `poll()` instead, so\n * the effect only restarts on `sessionId`/`recheckToken`/the numeric\n * options, never on a same-render-identity-churning function prop.\n */\n const buildUrlRef = useRef(buildUrl)\n buildUrlRef.current = buildUrl\n\n useEffect(() => {\n if (!sessionId) {\n setStatus('not_found')\n setOrder(null)\n return\n }\n\n setStatus('loading')\n setOrder(null)\n\n let cancelled = false\n let pollTimer: ReturnType<typeof setTimeout> | null = null\n let requestController: AbortController | null = null\n let attempt = 0\n\n /**\n * Terminal transition — idempotent (guarded by `cancelled`) so the\n * independent window timer and an in-flight poll can both race to call\n * it without double-firing side effects like `clearCart()`.\n */\n const finish = (nextStatus: ShopOrderStatusState) => {\n if (cancelled) return\n cancelled = true\n clearTimeout(windowTimer)\n if (pollTimer) clearTimeout(pollTimer)\n requestController?.abort()\n setStatus(nextStatus)\n }\n\n // REB-837 review fix: enforces the WHOLE polling window independently\n // of any request's own outcome — a `fetch` that never settles (hangs\n // forever) must not leave `status` stuck on `'loading'` indefinitely.\n const windowTimer = setTimeout(() => finish('pending_timeout'), timeoutMs)\n\n const poll = async () => {\n if (cancelled) return\n\n requestController = new AbortController()\n const requestTimer = setTimeout(() => requestController?.abort(), requestTimeoutMs)\n\n try {\n const res = await fetch(buildUrlRef.current(sessionId), {\n signal: requestController.signal,\n cache: 'no-store',\n })\n clearTimeout(requestTimer)\n if (cancelled) return\n\n if (res.status === 404) {\n finish('not_found')\n return\n }\n\n if (!res.ok) {\n finish('error')\n return\n }\n\n // REB-839: `createShopOrderRoute` forwards base-api's body verbatim,\n // and base-api wraps every payload as `{ data: <order> }` (`apiOk`).\n // Unwrap it; a bare order (a custom route that already unwrapped) is\n // still accepted as-is.\n const body = (await res.json()) as { data?: ShopOrder } | ShopOrder\n if (cancelled) return\n const data = (\n body && typeof body === 'object' && 'data' in body && body.data ? body.data : body\n ) as ShopOrder\n\n setOrder(data)\n\n switch (data.paymentStatus) {\n case 'PAID':\n // REB-837: the ONE place this hook mutates anything else — a\n // cart cleared on any other status would discard it on a\n // payment that never actually completed.\n useCartStore.getState().clearCart()\n finish('paid')\n return\n case 'FAILED':\n finish('failed')\n return\n case 'REFUNDED':\n finish('refunded')\n return\n case 'PARTIALLY_REFUNDED':\n finish('partially_refunded')\n return\n case 'PENDING':\n default: {\n if (cancelled) return\n const nextDelay = Math.min(intervalMs * backoffFactor ** attempt, maxIntervalMs)\n attempt += 1\n pollTimer = setTimeout(poll, nextDelay)\n }\n }\n } catch (err) {\n clearTimeout(requestTimer)\n if (cancelled) return\n if (err instanceof Error && err.name === 'AbortError') {\n // A single slow/hung request past `requestTimeoutMs` (or an\n // unmount/recheck abort, harmless since `cancelled` is already\n // true in that case and we returned above) — transient, retry\n // with backoff rather than treating ONE slow request as fatal.\n // The independent `windowTimer` is what ultimately bounds this.\n const nextDelay = Math.min(intervalMs * backoffFactor ** attempt, maxIntervalMs)\n attempt += 1\n pollTimer = setTimeout(poll, nextDelay)\n return\n }\n finish('error')\n }\n }\n\n void poll()\n\n return () => {\n cancelled = true\n clearTimeout(windowTimer)\n if (pollTimer) clearTimeout(pollTimer)\n requestController?.abort()\n }\n // `buildUrl` intentionally excluded — see the `buildUrlRef` docblock above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sessionId, recheckToken, timeoutMs, intervalMs, backoffFactor, maxIntervalMs, requestTimeoutMs])\n\n return { status, order, recheck }\n}\n","import { create } from 'zustand'\nimport { persist } from 'zustand/middleware'\n\n/**\n * `useCartStore` — the SDK's cart state (REB-829). Ported verbatim (same\n * shape, same quantity-clamping rules) from demo-site's local\n * `lib/store/cart-store.ts`, which every SITE was otherwise expected to\n * hand-copy. `CART_STORAGE_KEY` stays exactly `'shop-cart'` on purpose: a\n * SITE migrating from the local copy keeps its visitors' existing carts.\n *\n * `skipHydration: true` (zustand persist) is load-bearing for SSR: without\n * it, zustand's persist middleware reads `localStorage` synchronously during\n * module evaluation, which throws (or silently desyncs) on the server. The\n * `CartProvider` (`./provider`) calls `useCartStore.persist.rehydrate()`\n * inside a `useEffect`, so hydration only ever happens client-side, after\n * mount — the same explicit-hydration contract the SITE README documents.\n */\nexport const CART_STORAGE_KEY = 'shop-cart'\n\n/**\n * The zustand `persist` schema version for the cart's PERSISTED shape (the\n * `CartItem[]` written to `localStorage` under `CART_STORAGE_KEY`) — bump\n * this (with a `migrate` option) if that shape ever changes incompatibly.\n * Also folded into {@link CART_STORE_GLOBAL_KEY} (REB-837 review fix): a\n * future incompatible shape change should mint a NEW global singleton\n * rather than let a stale, already-evaluated old-shape instance (e.g. kept\n * alive across an HMR reload in dev) silently satisfy the new one.\n */\nexport const CART_STORE_PERSIST_VERSION = 0\n\nexport interface CartItem {\n productId: string\n variantId?: string | null\n name: string\n variantName?: string\n /** Integer cents — every Reblu shop price travels as cents, never a float. */\n unitPrice: number\n quantity: number\n imageUrl?: string\n /** `null` means untracked/unlimited stock — never clamp against it. */\n maxStock: number | null\n /** Basis points, e.g. `2100` = 21%. `null`/`undefined` = unknown or exempt. */\n taxRateBps?: number | null\n taxRateName?: string | null\n}\n\n/**\n * Outcome of `addItem` (REB-837): a SITE's add-to-cart feedback (toast,\n * `AddToCartButton`'s built-in confirmed/error state) needs to know what\n * actually happened, not just fire-and-forget — `'clamped'` only differs\n * from `'added'` in the RESULTING quantity reaching less than what was\n * requested (capped by `maxStock`) while the line still grew; `'refused'`\n * means nothing was added (invalid quantity, out of stock, or the line was\n * already at `maxStock` — REB-839).\n */\nexport type AddItemStatus = 'added' | 'clamped' | 'refused'\n\nexport interface AddItemOutcome {\n status: AddItemStatus\n /** The resulting line quantity after the call (`0` when refused and no line exists). */\n quantity: number\n}\n\nexport interface CartStore {\n items: CartItem[]\n addItem: (item: CartItem) => AddItemOutcome\n removeItem: (productId: string, variantId?: string | null) => void\n updateQuantity: (\n productId: string,\n variantId: string | null | undefined,\n quantity: number,\n ) => void\n clearCart: () => void\n getTotal: () => number\n getItemCount: () => number\n}\n\nfunction isSameItem(a: CartItem, productId: string, variantId?: string | null): boolean {\n return a.productId === productId && (a.variantId ?? null) === (variantId ?? null)\n}\n\n/**\n * REB-829 review finding: neither `addItem` nor `updateQuantity` validated\n * `quantity` — `0`, a negative number, `NaN` (e.g. a stray `parseInt('')`\n * upstream), a non-integer, or `maxStock` itself being `0` (out of stock)\n * all used to leave an invalid line sitting in the cart, silently reaching\n * checkout.\n *\n * Normalizes to a safe POSITIVE INTEGER clamped to `maxStock`. Returns `0`\n * to signal \"not a valid positive quantity\" — callers decide what that\n * means for them: `addItem` ignores the call (never creates/corrupts a\n * line), `updateQuantity` removes the line (same as its pre-existing\n * `quantity <= 0` → remove contract, just now reached by more inputs).\n *\n * `Math.trunc` rounds toward zero (not `Math.floor`, which would round a\n * negative input further negative before the `< 1` check below ever saw it\n * — not a behavioural difference here since any negative result is\n * rejected either way, but `trunc` is the operation that actually means\n * \"drop the fractional part\").\n */\nfunction normalizeQuantity(quantity: number, maxStock: number | null): number {\n if (!Number.isFinite(quantity)) return 0\n const truncated = Math.trunc(quantity)\n if (truncated < 1) return 0\n if (maxStock === null) return truncated\n const maxTruncated = Math.trunc(maxStock)\n if (maxTruncated < 1) return 0\n return Math.min(truncated, maxTruncated)\n}\n\n/**\n * REB-837 cross-entry singleton guard — the mutation that caught it: a\n * single test rendering `useShopOrderStatus` (the `./shop` tsup entry) and\n * asserting on `useCartStore` (the `./cart` entry) saw the order resolve to\n * `paid` but the cart stay non-empty. Root cause: `splitting: false` (R4,\n * `tsup.config.ts`) bundles each entry standalone, so a plain relative\n * import of this file from `./shop` INLINES a SEPARATE copy of this\n * `create(...)` call — a second, disconnected store instance with its own\n * state, not the one `@reblu/site-next/cart` consumers read. `clearCart()`\n * from inside the hook silently cleared the WRONG store.\n *\n * `globalThis` keyed by a `Symbol.for(...)` (same registry entry regardless\n * of which bundle's copy of this module evaluates it first) makes every\n * entry that imports this file — however many times esbuild inlines it —\n * resolve to the SAME store. Verified by mutation: removing this guard\n * (reverting to a bare `export const useCartStore = create(...)`) turns\n * `use-shop-order-status.test.ts`'s cart-clearing assertions red again once\n * the hook is exercised through its OWN entry rather than imported directly\n * within `cart`'s own test file (which never saw the bug, since it only\n * ever touches ONE copy of this module).\n */\nfunction createCartStore() {\n return create<CartStore>()(\n persist(\n (set, get) => ({\n items: [],\n\n addItem: (item) => {\n // REB-837: `set`'s updater can't return a value to the caller, so the\n // outcome is captured in this closure variable and returned after.\n let outcome: AddItemOutcome = { status: 'refused', quantity: 0 }\n\n set((state) => {\n const existing = state.items.find((i) => isSameItem(i, item.productId, item.variantId))\n\n if (existing) {\n // REB-829 review follow-up (a): incoming maxStock is the\n // FRESHEST stock signal available — if it says 0, the product is\n // confirmed out of stock right now, so the existing line is\n // removed outright (same contract as updateQuantity's own\n // \"quantity resolves to <1 → remove\"), not merely left alone.\n if (item.maxStock !== null && Math.trunc(item.maxStock) < 1) {\n outcome = { status: 'refused', quantity: 0 }\n return {\n items: state.items.filter((i) => !isSameItem(i, item.productId, item.variantId)),\n }\n }\n\n const requestedQty = existing.quantity + item.quantity\n const newQty = normalizeQuantity(requestedQty, item.maxStock)\n if (newQty < 1) {\n // Invalid delta (NaN/non-positive quantity) — leave the\n // existing, previously-valid line untouched rather than\n // destroying good data because of one bad call.\n outcome = { status: 'refused', quantity: existing.quantity }\n return {}\n }\n\n // REB-839: nothing was gained when stock caps the line at (or,\n // with fresher stock, below) what it already held — report that\n // as 'refused', never as a partial-success 'clamped'. The line is\n // still reconciled below to the fresher maxStock.\n outcome = {\n status:\n newQty <= existing.quantity\n ? 'refused'\n : newQty < requestedQty\n ? 'clamped'\n : 'added',\n quantity: newQty,\n }\n\n // Refresh maxStock with the incoming value on every successful\n // merge — the stale-maxStock finding: a stored line used to keep\n // its ORIGINAL maxStock forever, so a later updateQuantity call\n // clamped against stock data that could be long out of date.\n return {\n items: state.items.map((i) =>\n isSameItem(i, item.productId, item.variantId)\n ? { ...i, quantity: newQty, maxStock: item.maxStock }\n : i,\n ),\n }\n }\n\n const qty = normalizeQuantity(item.quantity, item.maxStock)\n if (qty < 1) {\n // Invalid quantity, or maxStock 0 (out of stock) — never create\n // a line nobody could actually buy.\n outcome = { status: 'refused', quantity: 0 }\n return {}\n }\n\n outcome = { status: qty < item.quantity ? 'clamped' : 'added', quantity: qty }\n return { items: [...state.items, { ...item, quantity: qty }] }\n })\n\n return outcome\n },\n\n removeItem: (productId, variantId) => {\n set((state) => ({\n items: state.items.filter((i) => !isSameItem(i, productId, variantId)),\n }))\n },\n\n updateQuantity: (productId, variantId, quantity) => {\n const existing = get().items.find((i) => isSameItem(i, productId, variantId))\n if (!existing) return\n\n const newQty = normalizeQuantity(quantity, existing.maxStock)\n if (newQty < 1) {\n get().removeItem(productId, variantId)\n return\n }\n\n set((state) => ({\n items: state.items.map((i) =>\n isSameItem(i, productId, variantId) ? { ...i, quantity: newQty } : i,\n ),\n }))\n },\n\n clearCart: () => set({ items: [] }),\n\n getTotal: () => get().items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0),\n\n getItemCount: () => get().items.reduce((sum, item) => sum + item.quantity, 0),\n }),\n {\n name: CART_STORAGE_KEY,\n version: CART_STORE_PERSIST_VERSION,\n skipHydration: true,\n },\n ),\n )\n}\n\ntype CartStoreApi = ReturnType<typeof createCartStore>\n\n/**\n * Versioned (REB-837 review fix) with {@link CART_STORE_PERSIST_VERSION} —\n * an incompatible shape change bumps that version, which mints a NEW\n * `Symbol.for(...)` registry key, so a stale old-shape singleton (e.g. kept\n * alive across an HMR reload in dev before a code change takes effect)\n * never satisfies the new one.\n */\nconst CART_STORE_GLOBAL_KEY = Symbol.for(\n `@reblu/site-next/cart-store@v${CART_STORE_PERSIST_VERSION}`,\n)\n\ninterface GlobalWithCartStore {\n [CART_STORE_GLOBAL_KEY]?: CartStoreApi\n}\n\nconst globalForCartStore = globalThis as typeof globalThis & GlobalWithCartStore\n\nexport const useCartStore: CartStoreApi =\n globalForCartStore[CART_STORE_GLOBAL_KEY] ??\n (globalForCartStore[CART_STORE_GLOBAL_KEY] = createCartStore())\n\n/**\n * TEST-ONLY (REB-837 review fix). Clears the `globalThis`-keyed slot so the\n * NEXT access to `useCartStore` — in THIS module instance — creates a fresh\n * store. The singleton is intentionally shared process-wide; most tests\n * should keep resetting STATE via `useCartStore.setState(...)` in\n * `beforeEach`, not the instance itself. Reach for this only to prove\n * something about the singleton mechanism (e.g. the cross-entry test in\n * `tests/cart-store-cross-entry-singleton.test.ts`) — it does not and\n * cannot affect an ALREADY-evaluated copy of this module inlined into a\n * different tsup entry's bundle (each such copy owns its own `const\n * globalForCartStore`/`CART_STORE_GLOBAL_KEY` closed over at that copy's\n * own module-evaluation time, same as any other ES module binding).\n */\nexport function __resetCartStoreForTests(): void {\n delete globalForCartStore[CART_STORE_GLOBAL_KEY]\n}\n","'use client'\n\nimport { useSearchParams } from 'next/navigation'\n\n/**\n * `useCheckoutReturn` (REB-837) — parses the standard return query\n * `createShopCheckoutRoute`'s default `successPath`/`cancelPath` land the\n * visitor on: `?pago=ok&session_id=...` or `?pago=cancelado`. A SITE using a\n * custom `successPath`/`cancelPath` that keeps the SAME `pago`/`session_id`\n * query keys still works with this hook unchanged.\n */\nexport type CheckoutReturnStatus = 'ok' | 'cancelled' | null\n\nexport interface UseCheckoutReturnResult {\n status: CheckoutReturnStatus\n sessionId: string | null\n}\n\nexport function useCheckoutReturn(): UseCheckoutReturnResult {\n const searchParams = useSearchParams()\n const pago = searchParams.get('pago')\n const sessionId = searchParams.get('session_id')\n\n const status: CheckoutReturnStatus =\n pago === 'ok' ? 'ok' : pago === 'cancelado' ? 'cancelled' : null\n\n return { status, sessionId }\n}\n"],"mappings":"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","names":["import_navigation"]}