@hartl-services/medusa-storefront 0.0.0-stage → 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 (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -2
  3. package/dist/browser/account-verification-channel.d.ts +11 -0
  4. package/dist/browser/account-verification-channel.js +32 -0
  5. package/dist/browser/actions.d.ts +49 -0
  6. package/dist/browser/actions.js +1 -0
  7. package/dist/browser/affiliate-referral.d.ts +152 -0
  8. package/dist/browser/affiliate-referral.js +85 -0
  9. package/dist/browser/browser-session.d.ts +3 -0
  10. package/dist/browser/browser-session.js +27 -0
  11. package/dist/browser/cart-mutations.d.ts +35 -0
  12. package/dist/browser/cart-mutations.js +137 -0
  13. package/dist/browser/cart-refresh.d.ts +11 -0
  14. package/dist/browser/cart-refresh.js +33 -0
  15. package/dist/browser/client.d.ts +15 -0
  16. package/dist/browser/client.js +20 -0
  17. package/dist/browser/fresh-order-marker.d.ts +11 -0
  18. package/dist/browser/fresh-order-marker.js +30 -0
  19. package/dist/browser/index.d.ts +942 -0
  20. package/dist/browser/index.js +70 -0
  21. package/dist/browser/query-invalidation.d.ts +4 -0
  22. package/dist/browser/query-invalidation.js +39 -0
  23. package/dist/browser/query-keys.d.ts +42 -0
  24. package/dist/browser/query-keys.js +26 -0
  25. package/dist/browser/query-options.d.ts +777 -0
  26. package/dist/browser/query-options.js +159 -0
  27. package/dist/browser/region.d.ts +5 -0
  28. package/dist/browser/region.js +15 -0
  29. package/dist/browser/unwrap-medusa-action.d.ts +7 -0
  30. package/dist/browser/unwrap-medusa-action.js +25 -0
  31. package/dist/core/action-result.d.ts +17 -0
  32. package/dist/core/action-result.js +12 -0
  33. package/dist/core/addresses.d.ts +46 -0
  34. package/dist/core/addresses.js +137 -0
  35. package/dist/core/affiliate/affiliate-application-errors.d.ts +2 -0
  36. package/dist/core/affiliate/affiliate-application-errors.js +14 -0
  37. package/dist/core/affiliate/affiliate-commission-presentation.d.ts +15 -0
  38. package/dist/core/affiliate/affiliate-commission-presentation.js +28 -0
  39. package/dist/core/affiliate/affiliate-coupon-code-presentation.d.ts +10 -0
  40. package/dist/core/affiliate/affiliate-coupon-code-presentation.js +22 -0
  41. package/dist/core/affiliate/affiliate-coupon-label.d.ts +3 -0
  42. package/dist/core/affiliate/affiliate-coupon-label.js +4 -0
  43. package/dist/core/affiliate/affiliate-date-range.d.ts +6 -0
  44. package/dist/core/affiliate/affiliate-date-range.js +16 -0
  45. package/dist/core/affiliate/affiliate-rate-presentation.d.ts +1 -0
  46. package/dist/core/affiliate/affiliate-rate-presentation.js +8 -0
  47. package/dist/core/affiliate/partner-company-address.d.ts +24 -0
  48. package/dist/core/affiliate/partner-company-address.js +31 -0
  49. package/dist/core/auth/customer-sign-in.d.ts +36 -0
  50. package/dist/core/auth/customer-sign-in.js +55 -0
  51. package/dist/core/auth/link-token.d.ts +8 -0
  52. package/dist/core/auth/link-token.js +11 -0
  53. package/dist/core/auth/login-code-auto-send.d.ts +7 -0
  54. package/dist/core/auth/login-code-auto-send.js +11 -0
  55. package/dist/core/auth/password-rule-label.d.ts +4 -0
  56. package/dist/core/auth/password-rule-label.js +13 -0
  57. package/dist/core/cart/cart-line-item-image-url.d.ts +9 -0
  58. package/dist/core/cart/cart-line-item-image-url.js +14 -0
  59. package/dist/core/cart/cart-normalization.d.ts +2 -0
  60. package/dist/core/cart/cart-normalization.js +11 -0
  61. package/dist/core/cart/cart-price-breakdown.d.ts +30 -0
  62. package/dist/core/cart/cart-price-breakdown.js +45 -0
  63. package/dist/core/cart/cart-tax-breakdown.d.ts +17 -0
  64. package/dist/core/cart/cart-tax-breakdown.js +18 -0
  65. package/dist/core/cart/coupon-promotions.d.ts +8 -0
  66. package/dist/core/cart/coupon-promotions.js +9 -0
  67. package/dist/core/cart/payment-session.d.ts +3 -0
  68. package/dist/core/cart/payment-session.js +12 -0
  69. package/dist/core/cart/refunded-cart.d.ts +27 -0
  70. package/dist/core/cart/refunded-cart.js +46 -0
  71. package/dist/core/catalog/inventory.d.ts +10 -0
  72. package/dist/core/catalog/inventory.js +9 -0
  73. package/dist/core/catalog/medusa-product-image.d.ts +8 -0
  74. package/dist/core/catalog/medusa-product-image.js +13 -0
  75. package/dist/core/catalog/product-price.d.ts +8 -0
  76. package/dist/core/catalog/product-price.js +21 -0
  77. package/dist/core/catalog/selected-variant.d.ts +7 -0
  78. package/dist/core/catalog/selected-variant.js +9 -0
  79. package/dist/core/checkout/checkout-account-offer-state.d.ts +41 -0
  80. package/dist/core/checkout/checkout-account-offer-state.js +32 -0
  81. package/dist/core/checkout/checkout-address-draft.d.ts +39 -0
  82. package/dist/core/checkout/checkout-address-draft.js +78 -0
  83. package/dist/core/checkout/checkout-address-form.d.ts +24 -0
  84. package/dist/core/checkout/checkout-address-form.js +99 -0
  85. package/dist/core/checkout/checkout-coupon.d.ts +9 -0
  86. package/dist/core/checkout/checkout-coupon.js +13 -0
  87. package/dist/core/checkout/checkout-steps.d.ts +20 -0
  88. package/dist/core/checkout/checkout-steps.js +44 -0
  89. package/dist/core/checkout/customer-login-required.d.ts +12 -0
  90. package/dist/core/checkout/customer-login-required.js +14 -0
  91. package/dist/core/checkout/payment-authorization.d.ts +8 -0
  92. package/dist/core/checkout/payment-authorization.js +9 -0
  93. package/dist/core/checkout/stripe-redirect-return.d.ts +13 -0
  94. package/dist/core/checkout/stripe-redirect-return.js +29 -0
  95. package/dist/core/errors.d.ts +12 -0
  96. package/dist/core/errors.js +18 -0
  97. package/dist/core/fields.d.ts +18 -0
  98. package/dist/core/fields.js +20 -0
  99. package/dist/core/form-values.d.ts +119 -0
  100. package/dist/core/form-values.js +169 -0
  101. package/dist/core/format/money.d.ts +7 -0
  102. package/dist/core/format/money.js +6 -0
  103. package/dist/core/i18n.d.ts +24 -0
  104. package/dist/core/i18n.js +15 -0
  105. package/dist/core/image-url.d.ts +18 -0
  106. package/dist/core/image-url.js +34 -0
  107. package/dist/core/index.d.ts +47 -0
  108. package/dist/core/index.js +48 -0
  109. package/dist/core/order/order-payment-summary.d.ts +15 -0
  110. package/dist/core/order/order-payment-summary.js +48 -0
  111. package/dist/core/plugin-features.d.ts +21 -0
  112. package/dist/core/plugin-features.js +30 -0
  113. package/dist/core/query-string.d.ts +6 -0
  114. package/dist/core/query-string.js +16 -0
  115. package/dist/core/region.d.ts +11 -0
  116. package/dist/core/region.js +13 -0
  117. package/dist/core/report-error.d.ts +12 -0
  118. package/dist/core/report-error.js +1 -0
  119. package/dist/core/routing-locale.d.ts +1 -0
  120. package/dist/core/routing-locale.js +25 -0
  121. package/dist/core/settle-within.d.ts +8 -0
  122. package/dist/core/settle-within.js +14 -0
  123. package/dist/core/tax-compliance/address-tax-identification.d.ts +37 -0
  124. package/dist/core/tax-compliance/address-tax-identification.js +51 -0
  125. package/dist/core/tax-compliance/tax-identification.d.ts +18 -0
  126. package/dist/core/tax-compliance/tax-identification.js +58 -0
  127. package/dist/core/texts-de.d.ts +134 -0
  128. package/dist/core/texts-de.js +150 -0
  129. package/dist/react/cart-error-boundary.d.ts +24 -0
  130. package/dist/react/cart-error-boundary.js +57 -0
  131. package/dist/react/context.d.ts +17 -0
  132. package/dist/react/context.js +46 -0
  133. package/dist/react/index.d.ts +12 -0
  134. package/dist/react/index.js +14 -0
  135. package/dist/react/use-account.d.ts +204 -0
  136. package/dist/react/use-account.js +284 -0
  137. package/dist/react/use-affiliate.d.ts +566 -0
  138. package/dist/react/use-affiliate.js +573 -0
  139. package/dist/react/use-auth.d.ts +129 -0
  140. package/dist/react/use-auth.js +370 -0
  141. package/dist/react/use-cart.d.ts +32 -0
  142. package/dist/react/use-cart.js +90 -0
  143. package/dist/react/use-checkout-address-form.d.ts +156 -0
  144. package/dist/react/use-checkout-address-form.js +95 -0
  145. package/dist/react/use-checkout.d.ts +268 -0
  146. package/dist/react/use-checkout.js +873 -0
  147. package/dist/react/use-countdown.d.ts +5 -0
  148. package/dist/react/use-countdown.js +23 -0
  149. package/dist/react/use-link-token.d.ts +12 -0
  150. package/dist/react/use-link-token.js +17 -0
  151. package/dist/react/use-plugin-feature.d.ts +16 -0
  152. package/dist/react/use-plugin-feature.js +31 -0
  153. package/dist/react/use-verification-watch.d.ts +7 -0
  154. package/dist/react/use-verification-watch.js +49 -0
  155. package/dist/registry/account.json +248 -0
  156. package/dist/registry/affiliate.json +302 -0
  157. package/dist/registry/cart.json +127 -0
  158. package/dist/registry/checkout.json +169 -0
  159. package/dist/registry/form.json +145 -0
  160. package/dist/registry/registry.json +917 -0
  161. package/dist/registry/setup.json +76 -0
  162. package/dist/server/affiliate.d.ts +124 -0
  163. package/dist/server/affiliate.js +145 -0
  164. package/dist/server/cart.d.ts +10 -0
  165. package/dist/server/cart.js +113 -0
  166. package/dist/server/catalog.d.ts +20 -0
  167. package/dist/server/catalog.js +66 -0
  168. package/dist/server/checkout.d.ts +6 -0
  169. package/dist/server/checkout.js +12 -0
  170. package/dist/server/context.d.ts +38 -0
  171. package/dist/server/context.js +34 -0
  172. package/dist/server/customer.d.ts +68 -0
  173. package/dist/server/customer.js +276 -0
  174. package/dist/server/documents.d.ts +29 -0
  175. package/dist/server/documents.js +55 -0
  176. package/dist/server/index.d.ts +248 -0
  177. package/dist/server/index.js +45 -0
  178. package/dist/server/orders.d.ts +9 -0
  179. package/dist/server/orders.js +31 -0
  180. package/dist/server/plugin-features.d.ts +13 -0
  181. package/dist/server/plugin-features.js +52 -0
  182. package/dist/server/region.d.ts +6 -0
  183. package/dist/server/region.js +17 -0
  184. package/dist/server/route-response.d.ts +2 -0
  185. package/dist/server/route-response.js +17 -0
  186. package/dist/server/run-medusa-action.d.ts +7 -0
  187. package/dist/server/run-medusa-action.js +40 -0
  188. package/dist/server/session.d.ts +29 -0
  189. package/dist/server/session.js +106 -0
  190. package/dist/server/tax-compliance.d.ts +26 -0
  191. package/dist/server/tax-compliance.js +85 -0
  192. package/package.json +86 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hartl Services GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,93 @@
1
- # Temporary Holding Version
1
+ # @hartl-services/medusa-storefront
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Headless storefront for Next.js shops on a Medusa backend with the
4
+ `@hartl-services/medusa-*` plugins: cart, checkout (Stripe), customer account,
5
+ UID check and partner portal. The package holds the commerce logic, the Next
6
+ server/browser adapters and the React hooks; the shop keeps only its markup,
7
+ CMS content and tracking. Bug fixes reach every shop through a version bump.
8
+
9
+ Next.js App Router (16.2+) and React 19.2+ only. ESM only.
10
+
11
+ ```bash
12
+ pnpm add @hartl-services/medusa-storefront @hartl-services/medusa-contracts \
13
+ @medusajs/framework@^2.19.0 @medusajs/js-sdk @medusajs/types \
14
+ @tanstack/react-query @tanstack/react-form \
15
+ @stripe/stripe-js @stripe/react-stripe-js zod
16
+ ```
17
+
18
+ `@medusajs/framework` is the peer of `@hartl-services/medusa-contracts`, whose
19
+ schemas the package validates with. Install exactly one copy of Contracts
20
+ (`pnpm why @hartl-services/medusa-contracts`).
21
+
22
+ ## Subpaths
23
+
24
+ | Import | Runs | Contents |
25
+ |---|---|---|
26
+ | `/core` | anywhere | Pure logic: cart and price breakdown, coupon and payment rules, address drafts and schema factories, UID feedback, plugin-feature evaluation, `fields` constants, text codes (`Translate`, `createTranslator`, `StorefrontError`). |
27
+ | `/core/texts-de` | anywhere | German wording of every text code (`textsDe`, `translateDe`). |
28
+ | `/server` | Next server | `createStorefrontServer(config)`: session cookies, every server function behind the shop's Server Actions, cached loaders (region, plugin features, merchant country), Route Handlers (document download, partner portal reads). |
29
+ | `/browser` | browser | `createStorefrontBrowser(config)`: credentialed Medusa client, cart mutations, referral capture, react-query options, Stripe loader. |
30
+ | `/react` | browser | `StorefrontProvider` and the hooks for cart, sign-in, account, checkout, UID check and partner portal. |
31
+
32
+ ## A new shop
33
+
34
+ The package ships a [shadcn](https://ui.shadcn.com) registry of unstyled
35
+ copy-in components that wire the hooks together. Add it to the shop's
36
+ `components.json`, pinned to the installed package version:
37
+
38
+ ```json
39
+ {
40
+ "registries": {
41
+ "@hartl-storefront": "https://cdn.jsdelivr.net/npm/@hartl-services/medusa-storefront@0.1.0/dist/registry/{name}.json"
42
+ }
43
+ }
44
+ ```
45
+
46
+ ```bash
47
+ pnpm dlx shadcn@latest add @hartl-storefront/setup @hartl-storefront/form @hartl-storefront/cart @hartl-storefront/checkout @hartl-storefront/account @hartl-storefront/affiliate
48
+ ```
49
+
50
+ | Item | Adds |
51
+ |---|---|
52
+ | `setup` | `lib/storefront/{server,client,actions,texts,report-error,public-env,image-url}.ts`, `components/storefront/{storefront-providers,storefront-root}.tsx` |
53
+ | `form` | `hooks/use-app-form.ts` and the field components it registers |
54
+ | `cart` | cart widget, line items, totals, add-to-cart, cart page |
55
+ | `checkout` | checkout page, address, shipping/payment (Stripe), coupon, order confirmation |
56
+ | `account` | sign-in (password, code), password reset, verification, addresses, profile, orders, document download route |
57
+ | `affiliate` | partner portal: registration with UID check, links, coupons, commissions, consignment, its Route Handlers |
58
+
59
+ Then wrap the content of `app/(frontend)/[countryCode]/layout.tsx` in
60
+ `<StorefrontRoot>` and set the environment:
61
+
62
+ | Variable | |
63
+ |---|---|
64
+ | `NEXT_PUBLIC_MEDUSA_BACKEND_URL` | public Medusa URL (browser and images) |
65
+ | `MEDUSA_BACKEND_URL` | optional internal URL for server calls |
66
+ | `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` | Medusa publishable key |
67
+ | `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe key; without it the payment step shows "not configured" |
68
+
69
+ The Medusa backend's `STORE_CORS` and `AUTH_CORS` must contain the shop's
70
+ origin (the browser calls Medusa with credentials).
71
+
72
+ The copied files belong to the shop: style them, rename them, change the
73
+ wording. Keep the commerce logic in the package — a fix there reaches the shop
74
+ with the next version; a fix only in a copied file does not.
75
+
76
+ ## Wiring rules
77
+
78
+ - Create `createStorefrontServer` and `createStorefrontBrowser` once each, at
79
+ module level (`lib/storefront/server.ts`, `lib/storefront/client.ts`). The
80
+ server's per-request cache and the browser's shared state (refunded-cart
81
+ replacements, in-flight referral captures) rely on a single instance.
82
+ - Every server function the browser calls goes through a `"use server"`
83
+ wrapper defined in the shop (`lib/storefront/actions.ts`); Next accepts no
84
+ Server Action from a package. Never re-export from a `"use server"` file.
85
+ - Texts: the package returns text codes; `t` (`translateDe`, or
86
+ `createTranslator(textsDe, "de-DE", overrides)`) turns them into wording.
87
+ - Tracking goes into the browser config's `callbacks` (`onAddedToCart`,
88
+ `onCheckoutStarted`, `onPaymentMethodSelected`, `onLoggedOut`); each fires
89
+ once from the package's hooks.
90
+ - Errors: `reportError(error, { area, context, tags })` receives everything
91
+ the package reports; hand it to the shop's error tracking.
92
+ - Forms: the shop's TanStack `useAppForm` (its field components are the
93
+ design) takes the `formOptions` a hook returns.
@@ -0,0 +1,11 @@
1
+ export type AccountVerificationMessage = {
2
+ type: "email-verified";
3
+ } | {
4
+ type: "signed-in";
5
+ path: string;
6
+ };
7
+ export declare function parseAccountVerificationMessage(data: unknown): AccountVerificationMessage | null;
8
+ export declare function canReachOtherTabs(): boolean;
9
+ /** Null where the browser has no BroadcastChannel; callers then simply skip the shortcut. */
10
+ export declare function openAccountVerificationChannel(): BroadcastChannel | null;
11
+ export declare function postAccountVerificationMessage(message: AccountVerificationMessage): void;
@@ -0,0 +1,32 @@
1
+ // Tabs of one browser talk about a pending e-mail confirmation: the tab that
2
+ // opened the confirmation link announces it, the tab that waits with the login
3
+ // signs in and names the page the visitor came from.
4
+ const CHANNEL_NAME = "account-verification";
5
+ // Only pages that show the login form; never an arbitrary path another tab sends.
6
+ const RETURN_PATH = /^\/[a-z]{2}\/(checkout|account|partnerportal)(\/[a-z0-9-]+)*$/;
7
+ export function parseAccountVerificationMessage(data) {
8
+ if (typeof data !== "object" || data === null) {
9
+ return null;
10
+ }
11
+ const type = Reflect.get(data, "type");
12
+ if (type === "email-verified") {
13
+ return { type };
14
+ }
15
+ const path = Reflect.get(data, "path");
16
+ if (type === "signed-in" && typeof path === "string" && RETURN_PATH.test(path)) {
17
+ return { type, path };
18
+ }
19
+ return null;
20
+ }
21
+ export function canReachOtherTabs() {
22
+ return typeof BroadcastChannel !== "undefined";
23
+ }
24
+ /** Null where the browser has no BroadcastChannel; callers then simply skip the shortcut. */
25
+ export function openAccountVerificationChannel() {
26
+ return canReachOtherTabs() ? new BroadcastChannel(CHANNEL_NAME) : null;
27
+ }
28
+ export function postAccountVerificationMessage(message) {
29
+ const channel = openAccountVerificationChannel();
30
+ channel?.postMessage(message);
31
+ channel?.close();
32
+ }
@@ -0,0 +1,49 @@
1
+ import type { StorefrontServer } from "../server/index.js";
2
+ type Server = StorefrontServer;
3
+ /**
4
+ * The shop's Server Actions, one per server function: Next.js only accepts
5
+ * `"use server"` functions defined in the app, so the shop wraps each server
6
+ * function and hands the wrappers to the browser side here.
7
+ */
8
+ export interface StorefrontActions {
9
+ getActiveCart: Server["cart"]["getActiveCart"];
10
+ applyCustomerDefaultsToActiveCart: Server["cart"]["applyCustomerDefaultsToActiveCart"];
11
+ syncCartId: Server["cart"]["syncCartId"];
12
+ clearCartId: Server["cart"]["clearCartId"];
13
+ discardActiveCart: Server["cart"]["discardActiveCart"];
14
+ retrieveCustomer: Server["customer"]["retrieveCustomer"];
15
+ loginCustomer: Server["customer"]["loginCustomer"];
16
+ checkCustomerVerification: Server["customer"]["checkCustomerVerification"];
17
+ registerCustomer: Server["customer"]["registerCustomer"];
18
+ requestLoginCode: Server["customer"]["requestLoginCode"];
19
+ loginWithCode: Server["customer"]["loginWithCode"];
20
+ requestCustomerPasswordReset: Server["customer"]["requestCustomerPasswordReset"];
21
+ resetCustomerPassword: Server["customer"]["resetCustomerPassword"];
22
+ confirmCustomerEmail: Server["customer"]["confirmCustomerEmail"];
23
+ resendCustomerVerification: Server["customer"]["resendCustomerVerification"];
24
+ fetchCustomerPasswordPolicy: Server["customer"]["fetchCustomerPasswordPolicy"];
25
+ updateCustomer: Server["customer"]["updateCustomer"];
26
+ createCustomerAddress: Server["customer"]["createCustomerAddress"];
27
+ saveFirstCheckoutAddresses: Server["customer"]["saveFirstCheckoutAddresses"];
28
+ updateCustomerAddress: Server["customer"]["updateCustomerAddress"];
29
+ deleteCustomerAddress: Server["customer"]["deleteCustomerAddress"];
30
+ logoutCustomer: Server["customer"]["logoutCustomer"];
31
+ listShippingOptions: Server["checkout"]["listShippingOptions"];
32
+ listPaymentProviders: Server["checkout"]["listPaymentProviders"];
33
+ listRegions: Server["region"]["listRegions"];
34
+ fetchPluginFeatures: Server["pluginFeatures"]["fetchPluginFeatures"];
35
+ checkCartTaxIdentification: Server["taxCompliance"]["checkCartTaxIdentification"];
36
+ listCustomerTaxIdentifications: Server["taxCompliance"]["listCustomerTaxIdentifications"];
37
+ verifyCustomerAddressTaxIdentification: Server["taxCompliance"]["verifyCustomerAddressTaxIdentification"];
38
+ submitAffiliateApplication: Server["affiliate"]["submitAffiliateApplication"];
39
+ applyAffiliateOwnOrderPromotion: Server["affiliate"]["applyAffiliateOwnOrderPromotion"];
40
+ createAffiliateSlug: Server["affiliate"]["createAffiliateSlug"];
41
+ updateAffiliateSlug: Server["affiliate"]["updateAffiliateSlug"];
42
+ createAffiliateCouponCode: Server["affiliate"]["createAffiliateCouponCode"];
43
+ rotateAffiliateCouponCode: Server["affiliate"]["rotateAffiliateCouponCode"];
44
+ revokeAffiliateCouponCode: Server["affiliate"]["revokeAffiliateCouponCode"];
45
+ createConsignmentReport: Server["affiliate"]["createConsignmentReport"];
46
+ listOrders: Server["orders"]["listOrders"];
47
+ listDocuments: Server["documents"]["listDocuments"];
48
+ }
49
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,152 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ import type { CredentialedMedusaClient } from "./client.js";
3
+ export interface CaptureAffiliateReferralInput {
4
+ cartId?: string;
5
+ slug: string;
6
+ }
7
+ type RefreshActiveCart = () => Promise<Pick<HttpTypes.StoreCart, "id"> | null | undefined>;
8
+ export declare function runAffiliateReferralCapture<T>(capture: () => Promise<T>): Promise<T>;
9
+ export declare function waitForAffiliateReferralCapture(): Promise<void>;
10
+ export declare function removeCapturedAffiliateReferral(searchParams: URLSearchParams, capturedSlug: string): URLSearchParams;
11
+ export declare function captureAffiliateReferralWithClient(sdk: CredentialedMedusaClient, input: CaptureAffiliateReferralInput): Promise<{
12
+ status: "captured" | "replaced" | "ignored_existing_assignment" | "kept_existing";
13
+ referral: {
14
+ id: string;
15
+ source: "link" | "coupon";
16
+ status: "active" | "replaced" | "ignored_existing_assignment" | "pending_email_validation" | "consumed" | "expired" | "invalidated";
17
+ affiliate: {
18
+ id: string;
19
+ status: "active";
20
+ };
21
+ captured_at: string;
22
+ expires_at: string;
23
+ } | null;
24
+ }>;
25
+ export declare function captureAffiliateReferralAfterCartRefreshWithClient(sdk: CredentialedMedusaClient, slug: string, refreshActiveCart: RefreshActiveCart): Promise<{
26
+ status: "captured" | "replaced" | "ignored_existing_assignment" | "kept_existing";
27
+ referral: {
28
+ id: string;
29
+ source: "link" | "coupon";
30
+ status: "active" | "replaced" | "ignored_existing_assignment" | "pending_email_validation" | "consumed" | "expired" | "invalidated";
31
+ affiliate: {
32
+ id: string;
33
+ status: "active";
34
+ };
35
+ captured_at: string;
36
+ expires_at: string;
37
+ } | null;
38
+ }>;
39
+ export interface ApplyCheckoutCouponOptions {
40
+ useAffiliatePath: boolean;
41
+ }
42
+ export declare function applyCheckoutCouponWithClient(sdk: CredentialedMedusaClient, cartId: string, code: string, { useAffiliatePath }: ApplyCheckoutCouponOptions): Promise<{
43
+ cart: HttpTypes.StoreCart;
44
+ kind: "promotion";
45
+ response?: undefined;
46
+ } | {
47
+ kind: "affiliate";
48
+ response: {
49
+ status: "captured" | "replaced" | "ignored_existing_assignment" | "kept_existing" | "pending_email_validation" | "expired" | "coupon_already_redeemed";
50
+ referral: {
51
+ id: string;
52
+ source: "link" | "coupon";
53
+ status: "active" | "replaced" | "ignored_existing_assignment" | "pending_email_validation" | "consumed" | "expired" | "invalidated";
54
+ affiliate: {
55
+ id: string;
56
+ status: "active";
57
+ };
58
+ captured_at: string;
59
+ expires_at: string;
60
+ } | null;
61
+ discount_applied: boolean;
62
+ message?: string | undefined;
63
+ };
64
+ cart?: undefined;
65
+ }>;
66
+ export declare function resolveCouponLabelsWithClient(sdk: CredentialedMedusaClient, codes: string[]): Promise<{
67
+ labels: {
68
+ code: string;
69
+ label: string;
70
+ discount_percentage: number;
71
+ }[];
72
+ }>;
73
+ /** The referral and coupon calls bound to the shop's credentialed client. */
74
+ export declare function createAffiliateReferral(createClient: () => CredentialedMedusaClient): {
75
+ runAffiliateReferralCapture: typeof runAffiliateReferralCapture;
76
+ waitForAffiliateReferralCapture: typeof waitForAffiliateReferralCapture;
77
+ removeCapturedAffiliateReferral: typeof removeCapturedAffiliateReferral;
78
+ captureAffiliateReferral: (input: CaptureAffiliateReferralInput) => Promise<{
79
+ status: "captured" | "replaced" | "ignored_existing_assignment" | "kept_existing";
80
+ referral: {
81
+ id: string;
82
+ source: "link" | "coupon";
83
+ status: "active" | "replaced" | "ignored_existing_assignment" | "pending_email_validation" | "consumed" | "expired" | "invalidated";
84
+ affiliate: {
85
+ id: string;
86
+ status: "active";
87
+ };
88
+ captured_at: string;
89
+ expires_at: string;
90
+ } | null;
91
+ }>;
92
+ captureAffiliateReferralAfterCartRefresh: (slug: string, refreshActiveCart: RefreshActiveCart) => Promise<{
93
+ status: "captured" | "replaced" | "ignored_existing_assignment" | "kept_existing";
94
+ referral: {
95
+ id: string;
96
+ source: "link" | "coupon";
97
+ status: "active" | "replaced" | "ignored_existing_assignment" | "pending_email_validation" | "consumed" | "expired" | "invalidated";
98
+ affiliate: {
99
+ id: string;
100
+ status: "active";
101
+ };
102
+ captured_at: string;
103
+ expires_at: string;
104
+ } | null;
105
+ }>;
106
+ applyCheckoutCoupon: (cartId: string, code: string, options: ApplyCheckoutCouponOptions) => Promise<{
107
+ cart: HttpTypes.StoreCart;
108
+ kind: "promotion";
109
+ response?: undefined;
110
+ } | {
111
+ kind: "affiliate";
112
+ response: {
113
+ status: "captured" | "replaced" | "ignored_existing_assignment" | "kept_existing" | "pending_email_validation" | "expired" | "coupon_already_redeemed";
114
+ referral: {
115
+ id: string;
116
+ source: "link" | "coupon";
117
+ status: "active" | "replaced" | "ignored_existing_assignment" | "pending_email_validation" | "consumed" | "expired" | "invalidated";
118
+ affiliate: {
119
+ id: string;
120
+ status: "active";
121
+ };
122
+ captured_at: string;
123
+ expires_at: string;
124
+ } | null;
125
+ discount_applied: boolean;
126
+ message?: string | undefined;
127
+ };
128
+ cart?: undefined;
129
+ }>;
130
+ getAffiliateCartReferral: (cartId: string) => Promise<{
131
+ referral: {
132
+ id: string;
133
+ source: "link" | "coupon";
134
+ status: "active" | "replaced" | "ignored_existing_assignment" | "pending_email_validation" | "consumed" | "expired" | "invalidated";
135
+ affiliate: {
136
+ id: string;
137
+ status: "active";
138
+ };
139
+ captured_at: string;
140
+ expires_at: string;
141
+ } | null;
142
+ }>;
143
+ resolveCouponLabels: (codes: string[]) => Promise<{
144
+ labels: {
145
+ code: string;
146
+ label: string;
147
+ discount_percentage: number;
148
+ }[];
149
+ }>;
150
+ };
151
+ export type BrowserAffiliateReferral = ReturnType<typeof createAffiliateReferral>;
152
+ export {};
@@ -0,0 +1,85 @@
1
+ import { FetchError } from "@medusajs/js-sdk";
2
+ import { affiliateStorePaths } from "@hartl-services/medusa-contracts/paths/affiliate";
3
+ // One per browser page: cart creation waits for every "?ref=" capture still in
4
+ // flight rather than losing it. Module state on purpose, so every caller in
5
+ // the page shares the same set.
6
+ const inFlightReferralCaptures = new Set();
7
+ export function runAffiliateReferralCapture(capture) {
8
+ const capturePromise = Promise.resolve().then(capture);
9
+ inFlightReferralCaptures.add(capturePromise);
10
+ void capturePromise.then(() => inFlightReferralCaptures.delete(capturePromise), () => inFlightReferralCaptures.delete(capturePromise));
11
+ return capturePromise;
12
+ }
13
+ export async function waitForAffiliateReferralCapture() {
14
+ while (inFlightReferralCaptures.size > 0) {
15
+ await Promise.allSettled(inFlightReferralCaptures);
16
+ }
17
+ }
18
+ export function removeCapturedAffiliateReferral(searchParams, capturedSlug) {
19
+ if (searchParams.get("ref") !== capturedSlug) {
20
+ return searchParams;
21
+ }
22
+ const nextParams = new URLSearchParams(searchParams);
23
+ nextParams.delete("ref");
24
+ return nextParams;
25
+ }
26
+ export async function captureAffiliateReferralWithClient(sdk, input) {
27
+ return await sdk.client.fetch(affiliateStorePaths.post.captureReferral, {
28
+ body: { slug: input.slug, ...(input.cartId ? { cart_id: input.cartId } : {}) },
29
+ credentials: "include",
30
+ method: "POST",
31
+ });
32
+ }
33
+ export async function captureAffiliateReferralAfterCartRefreshWithClient(sdk, slug, refreshActiveCart) {
34
+ return await runAffiliateReferralCapture(async () => {
35
+ const activeCart = await refreshActiveCart();
36
+ return await captureAffiliateReferralWithClient(sdk, { cartId: activeCart?.id, slug });
37
+ });
38
+ }
39
+ export async function applyCheckoutCouponWithClient(sdk, cartId, code, { useAffiliatePath }) {
40
+ if (!useAffiliatePath) {
41
+ const { cart } = await sdk.store.cart.addPromotions(cartId, { promo_codes: [code] });
42
+ return { cart, kind: "promotion" };
43
+ }
44
+ try {
45
+ const response = await sdk.client.fetch(affiliateStorePaths.post.applyAffiliateCoupon, {
46
+ body: { cart_id: cartId, code },
47
+ credentials: "include",
48
+ method: "POST",
49
+ });
50
+ return { kind: "affiliate", response };
51
+ }
52
+ catch (error) {
53
+ if (!(error instanceof FetchError) || error.status !== 404) {
54
+ throw error;
55
+ }
56
+ const { cart } = await sdk.store.cart.addPromotions(cartId, { promo_codes: [code] });
57
+ return { cart, kind: "promotion" };
58
+ }
59
+ }
60
+ export async function resolveCouponLabelsWithClient(sdk, codes) {
61
+ if (codes.length === 0) {
62
+ return { labels: [] };
63
+ }
64
+ return await sdk.client.fetch(affiliateStorePaths.post.resolveCouponLabels, {
65
+ body: { codes },
66
+ credentials: "include",
67
+ method: "POST",
68
+ });
69
+ }
70
+ /** The referral and coupon calls bound to the shop's credentialed client. */
71
+ export function createAffiliateReferral(createClient) {
72
+ return {
73
+ runAffiliateReferralCapture,
74
+ waitForAffiliateReferralCapture,
75
+ removeCapturedAffiliateReferral,
76
+ captureAffiliateReferral: (input) => captureAffiliateReferralWithClient(createClient(), input),
77
+ captureAffiliateReferralAfterCartRefresh: (slug, refreshActiveCart) => captureAffiliateReferralAfterCartRefreshWithClient(createClient(), slug, refreshActiveCart),
78
+ applyCheckoutCoupon: (cartId, code, options) => applyCheckoutCouponWithClient(createClient(), cartId, code, options),
79
+ getAffiliateCartReferral: async (cartId) => await createClient().client.fetch(affiliateStorePaths.get.cartReferral(cartId), {
80
+ credentials: "include",
81
+ method: "GET",
82
+ }),
83
+ resolveCouponLabels: (codes) => resolveCouponLabelsWithClient(createClient(), codes),
84
+ };
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { CredentialedMedusaClient } from "./client.js";
2
+ export declare function startMedusaBrowserSessionWithClient(sdk: CredentialedMedusaClient, token: string): Promise<void>;
3
+ export declare function endMedusaBrowserSessionWithClient(sdk: CredentialedMedusaClient): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { FetchError } from "@medusajs/js-sdk";
2
+ // Cart, payment-session and completion calls go to Medusa straight from the
3
+ // browser, where the storefront's HttpOnly JWT cookie is invisible - so
4
+ // without this, Medusa sees every such call as a guest and cannot tell a
5
+ // logged-in customer from a stranger using their email. A Medusa session
6
+ // cookie on the backend host gives those calls the customer's identity. The
7
+ // JWT is handed over exactly once here and never stored in the browser.
8
+ export async function startMedusaBrowserSessionWithClient(sdk, token) {
9
+ await sdk.client.fetch("/auth/session", {
10
+ method: "POST",
11
+ headers: { authorization: `Bearer ${token}` },
12
+ });
13
+ }
14
+ // A 401 means Medusa no longer has the session (expired, or the backend
15
+ // restarted with its in-memory session store) - the goal of ending it is
16
+ // already reached, so logout must go on instead of failing for good.
17
+ export async function endMedusaBrowserSessionWithClient(sdk) {
18
+ try {
19
+ await sdk.auth.logout();
20
+ }
21
+ catch (error) {
22
+ if (error instanceof FetchError && error.status === 401) {
23
+ return;
24
+ }
25
+ throw error;
26
+ }
27
+ }
@@ -0,0 +1,35 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ import type { StorefrontActions } from "./actions.js";
3
+ import type { BrowserAffiliateReferral } from "./affiliate-referral.js";
4
+ import type { CredentialedMedusaClient } from "./client.js";
5
+ interface CartMutationDependencies {
6
+ actions: Pick<StorefrontActions, "applyCustomerDefaultsToActiveCart" | "clearCartId" | "fetchPluginFeatures" | "getActiveCart" | "syncCartId">;
7
+ createClient: () => CredentialedMedusaClient;
8
+ referral: Pick<BrowserAffiliateReferral, "applyCheckoutCoupon" | "waitForAffiliateReferralCapture">;
9
+ reportCheckoutError: (error: unknown, context?: Record<string, unknown>) => void;
10
+ }
11
+ export declare function createCartMutations({ actions, createClient, referral, reportCheckoutError }: CartMutationDependencies): {
12
+ createCart: (data: HttpTypes.StoreCreateCart) => Promise<HttpTypes.StoreCart>;
13
+ replaceRefundedCart(cart: HttpTypes.StoreCart): Promise<HttpTypes.StoreCart | null>;
14
+ addCartLineItem: (cartId: string, data: HttpTypes.StoreAddCartLineItem) => Promise<HttpTypes.StoreCart>;
15
+ updateCartLineItem(cartId: string, lineItemId: string, data: HttpTypes.StoreUpdateCartLineItem): Promise<HttpTypes.StoreCart>;
16
+ removeCartLineItem(cartId: string, lineItemId: string): Promise<HttpTypes.StoreLineItemDeleteResponse>;
17
+ updateCart(cartId: string, data: HttpTypes.StoreUpdateCart): Promise<HttpTypes.StoreCart>;
18
+ addCartShippingMethod(cartId: string, data: HttpTypes.StoreAddCartShippingMethods): Promise<HttpTypes.StoreCart>;
19
+ addCartPromotion(cartId: string, data: HttpTypes.StoreCartAddPromotion): Promise<HttpTypes.StoreCart>;
20
+ removeCartPromotion(cartId: string, data: HttpTypes.StoreCartRemovePromotion): Promise<HttpTypes.StoreCart>;
21
+ initiateCartPayment(cartId: string, data: HttpTypes.StoreInitializePaymentSession): Promise<HttpTypes.StorePaymentCollection>;
22
+ checkCartCompletion(cartId: string): Promise<{
23
+ cart_id: string;
24
+ ready: true;
25
+ }>;
26
+ completeCart(cartId: string): Promise<HttpTypes.StoreCompleteCartResponse>;
27
+ /** Adds one unit of a variant, creating the cart first when there is none. */
28
+ addProductToCart({ activeCart, regionId, variantId }: {
29
+ activeCart: Pick<HttpTypes.StoreCart, "id"> | null;
30
+ regionId: string;
31
+ variantId: string;
32
+ }): Promise<Pick<HttpTypes.StoreCart, "id">>;
33
+ };
34
+ export type BrowserCartMutations = ReturnType<typeof createCartMutations>;
35
+ export {};
@@ -0,0 +1,137 @@
1
+ import { checkoutStorePaths } from "@hartl-services/medusa-contracts/paths/checkout";
2
+ import { buildReplacementCart, getCustomerCouponCodes } from "../core/cart/refunded-cart.js";
3
+ import { hasCartPromotionCode } from "../core/checkout/checkout-coupon.js";
4
+ import { isPluginFeatureEnabled } from "../core/plugin-features.js";
5
+ // One per browser page, shared by every active-cart fetch: concurrent fetches
6
+ // of a refunded cart share one replacement.
7
+ const refundedCartReplacements = new Map();
8
+ // Cart creation/mutation runs through the browser-credentialed client, not a
9
+ // Server Action - Medusa's own host-only cookies must be attached directly
10
+ // from the browser.
11
+ export function createCartMutations({ actions, createClient, referral, reportCheckoutError }) {
12
+ async function createCartNow(data) {
13
+ const sdk = createClient();
14
+ const { cart } = await sdk.store.cart.create(data);
15
+ await actions.syncCartId(cart.id);
16
+ return (await actions.applyCustomerDefaultsToActiveCart()) ?? cart;
17
+ }
18
+ // Best effort through the coupon field's own path (affiliate coupon first,
19
+ // Medusa promotion on 404): a code that no longer applies is simply left off.
20
+ // A shipping coupon cannot apply before a shipping method is chosen.
21
+ async function reapplyCouponCodes(codes) {
22
+ if (codes.length === 0) {
23
+ return;
24
+ }
25
+ const cart = await actions.getActiveCart();
26
+ if (!cart) {
27
+ return;
28
+ }
29
+ try {
30
+ const useAffiliatePath = isPluginFeatureEnabled(await actions.fetchPluginFeatures(), "affiliate", "coupons");
31
+ for (const code of codes.filter((code) => !hasCartPromotionCode(cart, code))) {
32
+ await referral.applyCheckoutCoupon(cart.id, code, { useAffiliatePath }).catch((error) => reportCheckoutError(error, { cartId: cart.id }));
33
+ }
34
+ }
35
+ catch (error) {
36
+ // The replacement cart already exists; only its coupons are missing.
37
+ reportCheckoutError(error, { cartId: cart.id });
38
+ }
39
+ }
40
+ async function createCart(data) {
41
+ await referral.waitForAffiliateReferralCapture();
42
+ return await createCartNow(data);
43
+ }
44
+ async function addCartLineItem(cartId, data) {
45
+ const sdk = createClient();
46
+ const { cart } = await sdk.store.cart.createLineItem(cartId, data);
47
+ return cart;
48
+ }
49
+ return {
50
+ createCart,
51
+ // Swaps a cart whose payment was refunded (see refunded-cart.ts) for a fresh
52
+ // one with the same contents, so the customer simply pays again. Concurrent
53
+ // active-cart fetches share one replacement. Deliberately skips the referral
54
+ // wait: a "?ref=" capture refreshes the active cart itself and would wait on
55
+ // this replacement in turn; it then captures onto the new cart.
56
+ replaceRefundedCart(cart) {
57
+ const pending = refundedCartReplacements.get(cart.id);
58
+ if (pending) {
59
+ return pending;
60
+ }
61
+ const replacement = createCartNow(buildReplacementCart(cart))
62
+ .then(() => reapplyCouponCodes(getCustomerCouponCodes(cart)))
63
+ .then(() => actions.getActiveCart())
64
+ .catch((error) => {
65
+ // The refunded cart stays usable for display; its own error handling applies.
66
+ reportCheckoutError(error, { cartId: cart.id });
67
+ return cart;
68
+ })
69
+ .finally(() => refundedCartReplacements.delete(cart.id));
70
+ refundedCartReplacements.set(cart.id, replacement);
71
+ return replacement;
72
+ },
73
+ addCartLineItem,
74
+ async updateCartLineItem(cartId, lineItemId, data) {
75
+ const sdk = createClient();
76
+ const { cart } = await sdk.store.cart.updateLineItem(cartId, lineItemId, data);
77
+ return cart;
78
+ },
79
+ async removeCartLineItem(cartId, lineItemId) {
80
+ const sdk = createClient();
81
+ return await sdk.store.cart.deleteLineItem(cartId, lineItemId);
82
+ },
83
+ async updateCart(cartId, data) {
84
+ const sdk = createClient();
85
+ const { cart } = await sdk.store.cart.update(cartId, data);
86
+ return cart;
87
+ },
88
+ async addCartShippingMethod(cartId, data) {
89
+ const sdk = createClient();
90
+ const { cart } = await sdk.store.cart.addShippingMethod(cartId, data);
91
+ return cart;
92
+ },
93
+ async addCartPromotion(cartId, data) {
94
+ const sdk = createClient();
95
+ const { cart } = await sdk.store.cart.addPromotions(cartId, data);
96
+ return cart;
97
+ },
98
+ async removeCartPromotion(cartId, data) {
99
+ const sdk = createClient();
100
+ const { cart } = await sdk.store.cart.removePromotions(cartId, data);
101
+ return cart;
102
+ },
103
+ async initiateCartPayment(cartId, data) {
104
+ const sdk = createClient();
105
+ const { cart } = await sdk.store.cart.retrieve(cartId);
106
+ return (await sdk.store.payment.initiatePaymentSession(cart, data)).payment_collection;
107
+ },
108
+ // Runs every check the completion runs, without completing. Stripe captures
109
+ // when the payment is confirmed, so a refusal must come before that, not
110
+ // after (Medusa then refunds). Same transport and credentials as completeCart,
111
+ // so the registered-email gate sees the same session.
112
+ async checkCartCompletion(cartId) {
113
+ const sdk = createClient();
114
+ return await sdk.client.fetch(checkoutStorePaths.post.completionCheck(cartId), {
115
+ credentials: "include",
116
+ method: "POST",
117
+ });
118
+ },
119
+ async completeCart(cartId) {
120
+ const sdk = createClient();
121
+ const result = await sdk.store.cart.complete(cartId);
122
+ if (result.type === "order") {
123
+ await actions.clearCartId();
124
+ }
125
+ return result;
126
+ },
127
+ /** Adds one unit of a variant, creating the cart first when there is none. */
128
+ async addProductToCart({ activeCart, regionId, variantId }) {
129
+ const cart = activeCart ?? (await createCart({ region_id: regionId }));
130
+ await addCartLineItem(cart.id, {
131
+ quantity: 1,
132
+ variant_id: variantId,
133
+ });
134
+ return cart;
135
+ },
136
+ };
137
+ }