create-magic-storefront 0.1.0

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/index.js +222 -0
  4. package/package.json +31 -0
  5. package/template/.env.example +22 -0
  6. package/template/AGENTS.md +56 -0
  7. package/template/CLAUDE.md +1 -0
  8. package/template/PAGES.md +154 -0
  9. package/template/README.md +31 -0
  10. package/template/_gitignore +5 -0
  11. package/template/app/account/page.tsx +117 -0
  12. package/template/app/api/magicstore/webhook/route.ts +9 -0
  13. package/template/app/cart/page.tsx +124 -0
  14. package/template/app/checkout/page.tsx +268 -0
  15. package/template/app/collections/[handle]/page.tsx +48 -0
  16. package/template/app/error.tsx +18 -0
  17. package/template/app/globals.css +268 -0
  18. package/template/app/layout.tsx +63 -0
  19. package/template/app/not-found.tsx +10 -0
  20. package/template/app/page.tsx +16 -0
  21. package/template/app/pages/[handle]/page.tsx +28 -0
  22. package/template/app/products/[handle]/page.tsx +47 -0
  23. package/template/app/providers.tsx +49 -0
  24. package/template/app/search/page.tsx +37 -0
  25. package/template/app/sitemap.ts +36 -0
  26. package/template/app/storefront-api/[...path]/route.ts +83 -0
  27. package/template/components/analytics-views.tsx +18 -0
  28. package/template/components/buy-box.tsx +86 -0
  29. package/template/components/cart-link.tsx +9 -0
  30. package/template/components/pager.tsx +25 -0
  31. package/template/components/product-grid.tsx +32 -0
  32. package/template/components/sections/announcement-bar.tsx +13 -0
  33. package/template/components/sections/banner.tsx +41 -0
  34. package/template/components/sections/collections.tsx +67 -0
  35. package/template/components/sections/deal-of-day.tsx +48 -0
  36. package/template/components/sections/index.tsx +66 -0
  37. package/template/components/sections/product-shelves.tsx +123 -0
  38. package/template/components/sections/shoppable-stories.tsx +39 -0
  39. package/template/components/sections/store-reviews.tsx +67 -0
  40. package/template/components/sections/types.ts +10 -0
  41. package/template/lib/api.ts +35 -0
  42. package/template/lib/errors.ts +9 -0
  43. package/template/lib/upstream.ts +12 -0
  44. package/template/llms.txt +128 -0
  45. package/template/next.config.ts +9 -0
  46. package/template/package.json +26 -0
  47. package/template/tsconfig.json +33 -0
@@ -0,0 +1,9 @@
1
+ import { MagicStoreError } from '@magicstoreai/hydrogen';
2
+
3
+ /** What to tell the buyer: the API's localized detail when there is one. */
4
+ export function describe(
5
+ error: unknown,
6
+ fallback = 'Something went wrong. Please try again.',
7
+ ): string {
8
+ return error instanceof MagicStoreError ? (error.detail ?? error.message) : fallback;
9
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Where the Storefront API is: `MAGICSTORE_API_URL` when set (a mock server, a staging API), else
3
+ * `https://<MAGICSTORE_SHOP_DOMAIN>/api/v2/storefront`.
4
+ */
5
+ export function upstreamBaseUrl(): string | null {
6
+ const explicit = process.env.MAGICSTORE_API_URL;
7
+ if (explicit) {
8
+ return explicit.replace(/\/+$/, '');
9
+ }
10
+ const domain = process.env.MAGICSTORE_SHOP_DOMAIN;
11
+ return domain ? `https://${domain}/api/v2/storefront` : null;
12
+ }
@@ -0,0 +1,128 @@
1
+ # MagicStore Storefront SDK
2
+
3
+ > TypeScript packages for building a storefront on the MagicStore Storefront API v2
4
+ > (`/api/v2/storefront/*`). One backend serves every shop; a storefront only renders what the API
5
+ > returns. Prices, stock, discounts and delivery are the server's — the SDK formats, caches and
6
+ > keeps client state, it never computes them.
7
+
8
+ Every storefront — hand-written or generated — imports the SDK **only** through these entry points:
9
+
10
+ | Import | What | Runs |
11
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------- |
12
+ | `@magicstoreai/storefront-client` | `createStorefrontClient`: one typed method per API `operationId`, `MagicStoreError`, `paginate`, `Schema<>` | anywhere, no React |
13
+ | `@magicstoreai/hydrogen` | React: `MagicStoreProvider`, `useCart`, `useCustomer`, `useWishlist`, `<Money>`, `<Image>`, … | client (`"use client"`) |
14
+ | `@magicstoreai/hydrogen/core` | The same state without React: `CartController`, `CustomerSessionController`, `formatMoney`, variants | anywhere |
15
+ | `@magicstoreai/hydrogen/server` | `nextCacheFetch` (cache tags), `createWebhookHandler` (signed webhooks → revalidation) | server, edge |
16
+ | `@magicstoreai/hydrogen/seo` | `pageMeta`, `productJsonLd`, `breadcrumbJsonLd`, `jsonLdScript` | anywhere |
17
+
18
+ Never import from `src/` or `dist/` of a package, and never call `/api/v2/storefront/…` by URL —
19
+ call the client method. `npx create-magic-storefront check` fails on either.
20
+
21
+ ## Start
22
+
23
+ ```bash
24
+ npm create magic-storefront@latest my-shop -- --shop shop.example.uz
25
+ ```
26
+
27
+ scaffolds a Next.js App Router storefront (home from the merchant's sections, catalog, product,
28
+ search, cart, checkout, sign-in, orders, SEO, webhook revalidation). Or install the packages:
29
+ `npm i @magicstoreai/storefront-client @magicstoreai/hydrogen`.
30
+
31
+ ## Docs
32
+
33
+ - `PAGES.md` (in a scaffolded storefront; `examples/starter/PAGES.md` in the SDK repository): how to
34
+ build each page type — home from the `/home` sections, collection, product, search, cart,
35
+ checkout, account — with its data calls, cache tags, SEO and required states.
36
+ - [Hydrogen catalogue](node_modules/@magicstoreai/hydrogen/CATALOGUE.md): every export with its props
37
+ or signature, an example, and the API operations it calls. In the SDK repository:
38
+ `packages/hydrogen/CATALOGUE.md`.
39
+ - [Client README](node_modules/@magicstoreai/storefront-client/README.md): options, headers,
40
+ idempotency, retries, errors, pagination.
41
+ - [Hydrogen README](node_modules/@magicstoreai/hydrogen/README.md): provider, hooks, server helpers.
42
+ - The API itself (normative): the backend repository's `docs/api/v2/` — `standards.md` (wire format,
43
+ every error code), `authentication.md`, `cart.md`, `checkout.md`, `customer.md`, `webhooks.md`.
44
+ Operation shapes: `Schema<'Name'>` and the client's method types, generated from the OpenAPI spec.
45
+
46
+ ## Two credentials
47
+
48
+ - **Which shop** — a storefront access token (`storefrontToken`, header `X-Storefront-Token`), or
49
+ else the request host must be one of the shop's domains. A token may list browser origins; from
50
+ any other origin the API answers `ORIGIN_NOT_ALLOWED`. Without a token for the browser, proxy
51
+ browser calls through your own server (the starter's `app/storefront-api/[...path]`).
52
+ - **Which customer** — a customer session: access token (60 min) + refresh token (30 days,
53
+ single-use), only on customer routes. `useCustomer()` signs in (OTP, Telegram, OQ, Click),
54
+ refreshes and signs out; `MagicStoreProvider` keeps the session in storage.
55
+
56
+ Public catalog reads need no customer. Render them on the server with a client whose `fetch` is
57
+ `nextCacheFetch()` so the webhook can revalidate them by tag.
58
+
59
+ ## Wire rules
60
+
61
+ - Money is `{ amount: "12500.00", currencyCode }` — a decimal **string**, already rounded. Render it
62
+ with `<Money>` / `formatMoney` (the shop's `moneyFormat`); never parse it to a float, add it up
63
+ or round it again. Totals come from the cart and the checkout.
64
+ - Every `id` and `*Id` is a string. Enums are `UPPER_SNAKE_CASE`. Links use `handle`.
65
+ - Lists paginate with `page` + `perPage` (default 24, max 100); read `meta.pagination`, or
66
+ `<Pagination>`. Sorts order (`sort=-createdAt`), filters narrow (`filter[onSale]=true`).
67
+ - A missing value is `null`, never `0` or `""`: show a fallback.
68
+ - The home page is `GET /home`: an ordered list of sections, one `type` each
69
+ (`GET /theme/section-schema` has every type's JSON Schema). Render an unknown type as nothing.
70
+
71
+ ## Credentials in the browser
72
+
73
+ The cart id and the checkout id are credentials. Never log them and never put them in a URL or
74
+ query string. A guest proves an order with the `X-Checkout-Id` header. The same goes for tokens,
75
+ OTP codes and full phone numbers.
76
+
77
+ ## Cart → checkout → payment
78
+
79
+ 1. The first `useCart().addLine(…)` creates the cart; its id is stored for you. Every cart answer
80
+ is fully priced: render `cart.cost`, never compute totals. Availability on the cart is soft —
81
+ there is no stock reservation; stock is taken at checkout completion.
82
+ 2. Signing in may move the lines into the customer's cart (a new cart id) — the provider handles it.
83
+ 3. `checkoutsStore({ body: { cartId } })` is safe to repeat; drive the form from `checkout.missing`
84
+ (contact, address or pickup point, delivery option, payment method).
85
+ 4. Delivery options are quotes: after any change to the address, pickup point or cart, list them
86
+ again and select by `id`.
87
+ 5. `checkoutsCompletion` needs an `Idempotency-Key` — the client adds one. Retrying the same
88
+ purchase, pass the same key: `client.checkoutsCompletion(input, { idempotencyKey })` with the
89
+ failed call's `error.idempotencyKey`; the same key never places a second order.
90
+ 6. After the payment page, read `ordersPayment` for the real status; never trust the redirect.
91
+
92
+ ## Errors → what the buyer sees
93
+
94
+ Every failure is a `MagicStoreError`. Branch on `error.code`, show `error.detail` (localized, safe
95
+ for buyers), quote `error.requestId` when reporting. GET calls retry on 429 / 5xx by themselves.
96
+
97
+ | `code` | Do |
98
+ | ------------------------------------------------ | ------------------------------------------------------------ |
99
+ | `NOT_FOUND` | Render the 404 page. |
100
+ | `VALIDATION_FAILED` | Show `errors[].message` next to each `errors[].field`. |
101
+ | `UNAUTHENTICATED` | Sign the customer in again. |
102
+ | `RATE_LIMITED`, `SERVICE_UNAVAILABLE` | Wait `retryAfter` seconds, then retry. |
103
+ | `STORE_UNAVAILABLE` | Show "temporarily closed"; browsing still works. |
104
+ | `STORE_NOT_FOUND`, `STORE_NOT_IDENTIFIED` | Configuration: wrong domain or token. |
105
+ | `ORIGIN_NOT_ALLOWED` | Configuration: add the site's origin to the token, or proxy. |
106
+ | `CART_NOT_FOUND`, `CART_CLOSED` | Start a new cart (the controller does). |
107
+ | `CART_EMPTY` | Back to the cart. |
108
+ | `CART_LINES_UNAVAILABLE` | Reload the cart; show each line's `issues`. |
109
+ | `CHECKOUT_NOT_READY` | Fill in what `checkout.missing` names. |
110
+ | `CHECKOUT_CLOSED` | Completed: show the order. Expired: start a new checkout. |
111
+ | `CHECKOUT_IN_PROGRESS` | Retry with the same `Idempotency-Key` after `retryAfter`. |
112
+ | `DELIVERY_QUOTE_EXPIRED`, `DELIVERY_UNAVAILABLE` | List delivery options again; choose another. |
113
+ | `DISCOUNT_CODE_REJECTED` | Show the reason; remove the code. |
114
+ | `PAYMENT_PROVIDER_UNAVAILABLE` | Retry with the same key, or offer another payment method. |
115
+ | `OTP_INVALID` | Let the buyer re-type the code. |
116
+ | `OTP_EXPIRED` | Ask for a new code. |
117
+ | `NETWORK_ERROR`, `ABORTED` | No answer (client-side): offer to try again. |
118
+
119
+ The full list, with the `meta` each code carries, is in the backend's `docs/api/v2/standards.md`.
120
+
121
+ ## What the SDK does not do
122
+
123
+ - Compute or round prices, discounts, taxes, delivery fees or stock — the API returns them.
124
+ - Reserve stock, or decide whether a product can be bought — read `availableForSale` and line
125
+ `issues`.
126
+ - Personalise cached pages: anything personal (`FOR_YOU`, the cart, the customer) is read in the
127
+ browser or per request, never from a shared cache.
128
+ - Log. The SDK never writes to the console.
@@ -0,0 +1,9 @@
1
+ import type { NextConfig } from 'next';
2
+
3
+ const config: NextConfig = {
4
+ env: {
5
+ NEXT_PUBLIC_MAGICSTORE_SHOP_DOMAIN: process.env.MAGICSTORE_SHOP_DOMAIN ?? '',
6
+ },
7
+ };
8
+
9
+ export default config;
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "magic-storefront-starter",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@magicstoreai/hydrogen": "workspace:*",
14
+ "@magicstoreai/storefront-client": "workspace:*",
15
+ "next": "^16.3.6",
16
+ "react": "^19.3.0",
17
+ "react-dom": "^19.3.0",
18
+ "server-only": "^0.0.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^26.6.2",
22
+ "@types/react": "^19.3.0",
23
+ "@types/react-dom": "^19.3.0",
24
+ "typescript": "5.9.3"
25
+ }
26
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "ES2022"],
5
+ "strict": true,
6
+ "noEmit": true,
7
+ "module": "esnext",
8
+ "moduleResolution": "bundler",
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "jsx": "react-jsx",
12
+ "skipLibCheck": true,
13
+ "incremental": true,
14
+ "plugins": [
15
+ {
16
+ "name": "next"
17
+ }
18
+ ],
19
+ "paths": {
20
+ "@/*": ["./*"]
21
+ },
22
+ "allowJs": true,
23
+ "esModuleInterop": true
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts"
31
+ ],
32
+ "exclude": ["node_modules"]
33
+ }