@hartl-services/medusa-storefront 0.0.0-stage → 0.2.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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -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 +53 -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 +36 -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 +946 -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 +43 -0
  24. package/dist/browser/query-keys.js +28 -0
  25. package/dist/browser/query-options.d.ts +781 -0
  26. package/dist/browser/query-options.js +165 -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/category-filter.d.ts +16 -0
  72. package/dist/core/catalog/category-filter.js +33 -0
  73. package/dist/core/catalog/inventory.d.ts +10 -0
  74. package/dist/core/catalog/inventory.js +9 -0
  75. package/dist/core/catalog/medusa-product-image.d.ts +8 -0
  76. package/dist/core/catalog/medusa-product-image.js +13 -0
  77. package/dist/core/catalog/product-price.d.ts +8 -0
  78. package/dist/core/catalog/product-price.js +21 -0
  79. package/dist/core/catalog/selected-variant.d.ts +7 -0
  80. package/dist/core/catalog/selected-variant.js +9 -0
  81. package/dist/core/checkout/checkout-account-offer-state.d.ts +41 -0
  82. package/dist/core/checkout/checkout-account-offer-state.js +32 -0
  83. package/dist/core/checkout/checkout-address-draft.d.ts +39 -0
  84. package/dist/core/checkout/checkout-address-draft.js +78 -0
  85. package/dist/core/checkout/checkout-address-form.d.ts +24 -0
  86. package/dist/core/checkout/checkout-address-form.js +99 -0
  87. package/dist/core/checkout/checkout-coupon.d.ts +9 -0
  88. package/dist/core/checkout/checkout-coupon.js +13 -0
  89. package/dist/core/checkout/checkout-steps.d.ts +20 -0
  90. package/dist/core/checkout/checkout-steps.js +44 -0
  91. package/dist/core/checkout/customer-login-required.d.ts +12 -0
  92. package/dist/core/checkout/customer-login-required.js +14 -0
  93. package/dist/core/checkout/payment-authorization.d.ts +8 -0
  94. package/dist/core/checkout/payment-authorization.js +9 -0
  95. package/dist/core/checkout/stripe-redirect-return.d.ts +13 -0
  96. package/dist/core/checkout/stripe-redirect-return.js +29 -0
  97. package/dist/core/errors.d.ts +12 -0
  98. package/dist/core/errors.js +18 -0
  99. package/dist/core/fields.d.ts +18 -0
  100. package/dist/core/fields.js +20 -0
  101. package/dist/core/form-values.d.ts +162 -0
  102. package/dist/core/form-values.js +234 -0
  103. package/dist/core/format/money.d.ts +7 -0
  104. package/dist/core/format/money.js +6 -0
  105. package/dist/core/i18n.d.ts +24 -0
  106. package/dist/core/i18n.js +15 -0
  107. package/dist/core/image-url.d.ts +18 -0
  108. package/dist/core/image-url.js +34 -0
  109. package/dist/core/index.d.ts +48 -0
  110. package/dist/core/index.js +49 -0
  111. package/dist/core/order/order-payment-summary.d.ts +15 -0
  112. package/dist/core/order/order-payment-summary.js +48 -0
  113. package/dist/core/plugin-features.d.ts +21 -0
  114. package/dist/core/plugin-features.js +30 -0
  115. package/dist/core/query-string.d.ts +6 -0
  116. package/dist/core/query-string.js +16 -0
  117. package/dist/core/region.d.ts +11 -0
  118. package/dist/core/region.js +13 -0
  119. package/dist/core/report-error.d.ts +12 -0
  120. package/dist/core/report-error.js +1 -0
  121. package/dist/core/routing-locale.d.ts +1 -0
  122. package/dist/core/routing-locale.js +25 -0
  123. package/dist/core/settle-within.d.ts +8 -0
  124. package/dist/core/settle-within.js +14 -0
  125. package/dist/core/tax-compliance/address-tax-identification.d.ts +37 -0
  126. package/dist/core/tax-compliance/address-tax-identification.js +51 -0
  127. package/dist/core/tax-compliance/tax-identification.d.ts +18 -0
  128. package/dist/core/tax-compliance/tax-identification.js +58 -0
  129. package/dist/core/texts-de.d.ts +136 -0
  130. package/dist/core/texts-de.js +152 -0
  131. package/dist/react/cart-error-boundary.d.ts +24 -0
  132. package/dist/react/cart-error-boundary.js +57 -0
  133. package/dist/react/context.d.ts +17 -0
  134. package/dist/react/context.js +46 -0
  135. package/dist/react/index.d.ts +12 -0
  136. package/dist/react/index.js +14 -0
  137. package/dist/react/use-account.d.ts +214 -0
  138. package/dist/react/use-account.js +294 -0
  139. package/dist/react/use-affiliate.d.ts +566 -0
  140. package/dist/react/use-affiliate.js +573 -0
  141. package/dist/react/use-auth.d.ts +136 -0
  142. package/dist/react/use-auth.js +402 -0
  143. package/dist/react/use-cart.d.ts +57 -0
  144. package/dist/react/use-cart.js +137 -0
  145. package/dist/react/use-checkout-address-form.d.ts +156 -0
  146. package/dist/react/use-checkout-address-form.js +95 -0
  147. package/dist/react/use-checkout.d.ts +268 -0
  148. package/dist/react/use-checkout.js +873 -0
  149. package/dist/react/use-countdown.d.ts +5 -0
  150. package/dist/react/use-countdown.js +23 -0
  151. package/dist/react/use-link-token.d.ts +12 -0
  152. package/dist/react/use-link-token.js +17 -0
  153. package/dist/react/use-plugin-feature.d.ts +16 -0
  154. package/dist/react/use-plugin-feature.js +31 -0
  155. package/dist/react/use-verification-watch.d.ts +7 -0
  156. package/dist/react/use-verification-watch.js +49 -0
  157. package/dist/registry/account.json +248 -0
  158. package/dist/registry/affiliate.json +302 -0
  159. package/dist/registry/cart.json +127 -0
  160. package/dist/registry/checkout.json +169 -0
  161. package/dist/registry/form.json +145 -0
  162. package/dist/registry/registry.json +917 -0
  163. package/dist/registry/setup.json +76 -0
  164. package/dist/server/affiliate.d.ts +124 -0
  165. package/dist/server/affiliate.js +145 -0
  166. package/dist/server/cart.d.ts +10 -0
  167. package/dist/server/cart.js +113 -0
  168. package/dist/server/catalog.d.ts +28 -0
  169. package/dist/server/catalog.js +82 -0
  170. package/dist/server/checkout.d.ts +6 -0
  171. package/dist/server/checkout.js +12 -0
  172. package/dist/server/context.d.ts +38 -0
  173. package/dist/server/context.js +34 -0
  174. package/dist/server/customer.d.ts +84 -0
  175. package/dist/server/customer.js +362 -0
  176. package/dist/server/documents.d.ts +29 -0
  177. package/dist/server/documents.js +55 -0
  178. package/dist/server/index.d.ts +258 -0
  179. package/dist/server/index.js +45 -0
  180. package/dist/server/orders.d.ts +9 -0
  181. package/dist/server/orders.js +31 -0
  182. package/dist/server/plugin-features.d.ts +13 -0
  183. package/dist/server/plugin-features.js +52 -0
  184. package/dist/server/region.d.ts +6 -0
  185. package/dist/server/region.js +17 -0
  186. package/dist/server/route-response.d.ts +2 -0
  187. package/dist/server/route-response.js +17 -0
  188. package/dist/server/run-medusa-action.d.ts +7 -0
  189. package/dist/server/run-medusa-action.js +40 -0
  190. package/dist/server/session.d.ts +29 -0
  191. package/dist/server/session.js +106 -0
  192. package/dist/server/tax-compliance.d.ts +26 -0
  193. package/dist/server/tax-compliance.js +85 -0
  194. package/package.json +86 -3
@@ -0,0 +1,36 @@
1
+ import type Medusa from "@medusajs/js-sdk";
2
+ type MedusaClient = Medusa;
3
+ export interface CustomerSignInDependencies {
4
+ /** Client that sends the given token as bearer authorization. */
5
+ createAuthenticatedClient: (token: string) => MedusaClient;
6
+ /** Claims the active cart for the customer; called with a client carrying the final token. */
7
+ transferCart: (sdk: MedusaClient, customer: {
8
+ email: string;
9
+ id: string;
10
+ }) => Promise<unknown>;
11
+ /** Stores the final token in the HttpOnly session cookie. */
12
+ storeToken: (token: string) => Promise<void>;
13
+ }
14
+ /**
15
+ * Password login with one fallback: accounts restored via "Passwort vergessen"
16
+ * are stored with a lowercase address, older ones keep the spelling they were
17
+ * registered with. A 401 for an address with uppercase letters is therefore
18
+ * retried once with the lowercased address. Returns the address that worked.
19
+ */
20
+ export declare function loginWithEmailpass(sdk: MedusaClient, credentials: {
21
+ email: string;
22
+ password: string;
23
+ }): Promise<{
24
+ email: string;
25
+ result: import("@medusajs/js-sdk").AuthLoginResponse;
26
+ }>;
27
+ /**
28
+ * Shared end of every sign-in that ends with a valid token: make sure the
29
+ * customer exists (and the token carries it), claim the active cart and store
30
+ * the final token in the cookie. Nothing is stored when a step fails.
31
+ */
32
+ export declare function completeCustomerSignIn(token: string, email: string, dependencies: CustomerSignInDependencies): Promise<{
33
+ state: "success";
34
+ sessionToken: string;
35
+ }>;
36
+ export {};
@@ -0,0 +1,55 @@
1
+ import { FetchError } from "@medusajs/js-sdk";
2
+ import { StorefrontError } from "../errors.js";
3
+ /**
4
+ * Password login with one fallback: accounts restored via "Passwort vergessen"
5
+ * are stored with a lowercase address, older ones keep the spelling they were
6
+ * registered with. A 401 for an address with uppercase letters is therefore
7
+ * retried once with the lowercased address. Returns the address that worked.
8
+ */
9
+ export async function loginWithEmailpass(sdk, credentials) {
10
+ try {
11
+ return { email: credentials.email, result: await sdk.auth.login("customer", "emailpass", credentials) };
12
+ }
13
+ catch (error) {
14
+ const lowercased = credentials.email.toLowerCase();
15
+ if (!(error instanceof FetchError) || error.status !== 401 || lowercased === credentials.email) {
16
+ throw error;
17
+ }
18
+ return { email: lowercased, result: await sdk.auth.login("customer", "emailpass", { ...credentials, email: lowercased }) };
19
+ }
20
+ }
21
+ async function ensureCustomerAccount(email, sdk) {
22
+ try {
23
+ const response = await sdk.store.customer.retrieve();
24
+ return { created: false, customer: response.customer };
25
+ }
26
+ catch {
27
+ const { customer } = await sdk.store.customer.create({ email });
28
+ return { created: true, customer };
29
+ }
30
+ }
31
+ // A token issued before the customer existed has no customer id; only the
32
+ // refreshed token carries it, and only that one may reach the cookie and the
33
+ // browser session (otherwise later cart calls from the browser are blocked).
34
+ async function refreshAuthenticationToken(sdk) {
35
+ const refreshed = await sdk.auth.refresh();
36
+ if (typeof refreshed === "object" && "token" in refreshed && typeof refreshed.token === "string") {
37
+ return refreshed.token;
38
+ }
39
+ throw new StorefrontError("auth.unsupported_step");
40
+ }
41
+ /**
42
+ * Shared end of every sign-in that ends with a valid token: make sure the
43
+ * customer exists (and the token carries it), claim the active cart and store
44
+ * the final token in the cookie. Nothing is stored when a step fails.
45
+ */
46
+ export async function completeCustomerSignIn(token, email, dependencies) {
47
+ const { createAuthenticatedClient, storeToken, transferCart } = dependencies;
48
+ const { created, customer } = await ensureCustomerAccount(email, createAuthenticatedClient(token));
49
+ const finalToken = created ? await refreshAuthenticationToken(createAuthenticatedClient(token)) : token;
50
+ await transferCart(createAuthenticatedClient(finalToken), { id: customer.id, email });
51
+ await storeToken(finalToken);
52
+ // Returned so the browser can open its Medusa session (browser-session.ts);
53
+ // it is used once there and never stored client-side.
54
+ return { state: "success", sessionToken: finalToken };
55
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reads the token from the fragment of an e-mail link: the password reset
3
+ * (`/<countryCode>/reset-password#token=...`) and the account verification
4
+ * (`/<countryCode>/verify-account#token=...`). The e-mail plugin puts the token
5
+ * into the fragment on purpose: browsers never send it to a server, so it
6
+ * cannot end up in access logs or referrers.
7
+ */
8
+ export declare function readLinkToken(hash: string): string | undefined;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Reads the token from the fragment of an e-mail link: the password reset
3
+ * (`/<countryCode>/reset-password#token=...`) and the account verification
4
+ * (`/<countryCode>/verify-account#token=...`). The e-mail plugin puts the token
5
+ * into the fragment on purpose: browsers never send it to a server, so it
6
+ * cannot end up in access logs or referrers.
7
+ */
8
+ export function readLinkToken(hash) {
9
+ const token = new URLSearchParams(hash.replace(/^#/, "")).get("token")?.trim();
10
+ return token ? token : undefined;
11
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Whether a login code must be sent on its own now. `lastSentFor` is the
3
+ * address the component already sent one to (kept in a ref, so a re-render or
4
+ * React's StrictMode double effect does not send twice); the backend throttles
5
+ * on top of that.
6
+ */
7
+ export declare function shouldAutoSendLoginCode(lastSentFor: string | null, email: string): boolean;
@@ -0,0 +1,11 @@
1
+ import { normalizeLoginEmail } from "@hartl-services/medusa-contracts/schemas/customer-auth";
2
+ /**
3
+ * Whether a login code must be sent on its own now. `lastSentFor` is the
4
+ * address the component already sent one to (kept in a ref, so a re-render or
5
+ * React's StrictMode double effect does not send twice); the backend throttles
6
+ * on top of that.
7
+ */
8
+ export function shouldAutoSendLoginCode(lastSentFor, email) {
9
+ const normalizedEmail = normalizeLoginEmail(email);
10
+ return normalizedEmail !== "" && normalizedEmail !== lastSentFor;
11
+ }
@@ -0,0 +1,4 @@
1
+ import type { CustomerPasswordPolicy, PasswordRuleKey } from "@hartl-services/medusa-contracts/schemas/password-policy";
2
+ import type { Translate } from "../i18n.js";
3
+ /** Checklist text of one password rule; the length rule names the shop's minimum. */
4
+ export declare function getPasswordRuleLabel(rule: PasswordRuleKey, policy: CustomerPasswordPolicy, t: Translate): string;
@@ -0,0 +1,13 @@
1
+ /** Checklist text of one password rule; the length rule names the shop's minimum. */
2
+ export function getPasswordRuleLabel(rule, policy, t) {
3
+ switch (rule) {
4
+ case "min_length":
5
+ return t("password_rule.min_length", { count: policy.min_length });
6
+ case "mixed_case":
7
+ return t("password_rule.mixed_case");
8
+ case "digit":
9
+ return t("password_rule.digit");
10
+ case "symbol":
11
+ return t("password_rule.symbol");
12
+ }
13
+ }
@@ -0,0 +1,9 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ type LineItemWithImageSources = HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem;
3
+ /**
4
+ * The fixed image priority for cart and order lines: variant thumbnail, first
5
+ * usable variant image, item thumbnail, product thumbnail, first usable
6
+ * product image. Empty strings count as missing, so they fall through.
7
+ */
8
+ export declare function getCartLineItemImageUrl(item: LineItemWithImageSources): string | null;
9
+ export {};
@@ -0,0 +1,14 @@
1
+ const usable = (url) => (url?.trim() ? url : undefined);
2
+ /**
3
+ * The fixed image priority for cart and order lines: variant thumbnail, first
4
+ * usable variant image, item thumbnail, product thumbnail, first usable
5
+ * product image. Empty strings count as missing, so they fall through.
6
+ */
7
+ export function getCartLineItemImageUrl(item) {
8
+ return (usable(item.variant?.thumbnail) ??
9
+ item.variant?.images?.map((image) => usable(image.url)).find(Boolean) ??
10
+ usable(item.thumbnail) ??
11
+ usable(item.product?.thumbnail) ??
12
+ item.product?.images?.map((image) => usable(image.url)).find(Boolean) ??
13
+ null);
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ export declare function normalizeStoreCart(cart: HttpTypes.StoreCart): HttpTypes.StoreCart;
@@ -0,0 +1,11 @@
1
+ // Medusa's `deletePromotionsWorkflow` soft-deletes without removing the
2
+ // `cart_promotion` link, so the store cart response can contain `null`
3
+ // promotion entries. This is the single place where the cart is normalized
4
+ // before any component sees it - do not add `?.` guards downstream.
5
+ export function normalizeStoreCart(cart) {
6
+ const promotions = (cart.promotions ?? []);
7
+ return {
8
+ ...cart,
9
+ promotions: promotions.filter((promotion) => Boolean(promotion?.id)),
10
+ };
11
+ }
@@ -0,0 +1,30 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ type PriceBreakdownSource = Pick<HttpTypes.StoreCart, "discount_total" | "gift_card_total" | "original_item_total" | "original_shipping_total" | "shipping_total" | "tax_total" | "total">;
3
+ type PriceLineItemSource = Pick<HttpTypes.StoreCartLineItem, "discount_total" | "original_total" | "quantity" | "total" | "unit_price">;
4
+ export declare function isZeroAmount(amount: number): boolean;
5
+ export declare function getCartPriceBreakdown(cart: PriceBreakdownSource): {
6
+ discountTotal: number;
7
+ giftCardTotal: number;
8
+ itemTotal: number;
9
+ originalShippingTotal: number;
10
+ shippingTotal: number;
11
+ taxTotal: number;
12
+ total: number;
13
+ };
14
+ export declare function getCartLineItemPriceBreakdown(item: PriceLineItemSource): {
15
+ discountTotal: number;
16
+ originalTotal: number;
17
+ total: number;
18
+ unitPrice: number;
19
+ };
20
+ type ShippingMethodSource = Pick<HttpTypes.StoreCartShippingMethod, "original_total" | "shipping_option_id" | "total">;
21
+ /**
22
+ * The selected shipping option shows what the cart actually charges for it
23
+ * (e.g. 0 after a free-shipping promotion), with the list price as original;
24
+ * every other option shows its list price.
25
+ */
26
+ export declare function getShippingOptionPrice(optionId: string, optionAmount: number | null | undefined, shippingMethod: ShippingMethodSource | undefined): {
27
+ amount: number;
28
+ originalAmount: number | undefined;
29
+ } | null;
30
+ export {};
@@ -0,0 +1,45 @@
1
+ // Medusa's totals are floats (4.9 - 4.8999999999999995); anything below half a
2
+ // cent is a rounding artefact, not an amount.
3
+ const AMOUNT_EPSILON = 0.005;
4
+ export function isZeroAmount(amount) {
5
+ return Math.abs(amount) < AMOUNT_EPSILON;
6
+ }
7
+ export function getCartPriceBreakdown(cart) {
8
+ // A shipping discount (e.g. automatic free shipping) is shown on the
9
+ // shipping line as discounted shipping, so the discount line only keeps
10
+ // what was taken off the items.
11
+ const shippingDiscount = typeof cart.original_shipping_total === "number" && typeof cart.shipping_total === "number" ? Math.max(cart.original_shipping_total - cart.shipping_total, 0) : 0;
12
+ const itemDiscount = (cart.discount_total ?? 0) - shippingDiscount;
13
+ return {
14
+ discountTotal: isZeroAmount(itemDiscount) ? 0 : itemDiscount,
15
+ giftCardTotal: cart.gift_card_total,
16
+ itemTotal: cart.original_item_total,
17
+ originalShippingTotal: cart.original_shipping_total,
18
+ shippingTotal: cart.shipping_total,
19
+ taxTotal: cart.tax_total,
20
+ total: cart.total,
21
+ };
22
+ }
23
+ export function getCartLineItemPriceBreakdown(item) {
24
+ const originalTotal = item.original_total ?? item.unit_price * item.quantity;
25
+ const total = item.total ?? originalTotal;
26
+ const calculatedDiscount = Math.max(originalTotal - total, 0);
27
+ return {
28
+ discountTotal: item.discount_total && item.discount_total > 0 ? item.discount_total : calculatedDiscount,
29
+ originalTotal,
30
+ total,
31
+ unitPrice: item.unit_price,
32
+ };
33
+ }
34
+ /**
35
+ * The selected shipping option shows what the cart actually charges for it
36
+ * (e.g. 0 after a free-shipping promotion), with the list price as original;
37
+ * every other option shows its list price.
38
+ */
39
+ export function getShippingOptionPrice(optionId, optionAmount, shippingMethod) {
40
+ if (shippingMethod?.shipping_option_id === optionId && typeof shippingMethod.total === "number") {
41
+ const originalTotal = shippingMethod.original_total;
42
+ return { amount: shippingMethod.total, originalAmount: typeof originalTotal === "number" && originalTotal > shippingMethod.total ? originalTotal : undefined };
43
+ }
44
+ return typeof optionAmount === "number" ? { amount: optionAmount, originalAmount: undefined } : null;
45
+ }
@@ -0,0 +1,17 @@
1
+ interface TaxLineSource {
2
+ rate: number | null;
3
+ total: number;
4
+ }
5
+ interface TaxableSource {
6
+ tax_lines?: TaxLineSource[] | null;
7
+ }
8
+ interface TaxBreakdownSource {
9
+ items?: TaxableSource[] | null;
10
+ shipping_methods?: TaxableSource[] | null;
11
+ }
12
+ export interface TaxRateBreakdownLine {
13
+ amount: number;
14
+ rate: number;
15
+ }
16
+ export declare function getTaxRateBreakdown(source: TaxBreakdownSource): TaxRateBreakdownLine[];
17
+ export {};
@@ -0,0 +1,18 @@
1
+ export function getTaxRateBreakdown(source) {
2
+ const amountByRate = new Map();
3
+ const addTaxLines = (taxLines) => {
4
+ for (const taxLine of taxLines ?? []) {
5
+ if (typeof taxLine.rate !== "number" || !Number.isFinite(taxLine.rate)) {
6
+ continue;
7
+ }
8
+ amountByRate.set(taxLine.rate, (amountByRate.get(taxLine.rate) ?? 0) + taxLine.total);
9
+ }
10
+ };
11
+ for (const item of source.items ?? []) {
12
+ addTaxLines(item.tax_lines);
13
+ }
14
+ for (const shippingMethod of source.shipping_methods ?? []) {
15
+ addTaxLines(shippingMethod.tax_lines);
16
+ }
17
+ return [...amountByRate.entries()].map(([rate, amount]) => ({ amount, rate })).sort((a, b) => b.rate - a.rate);
18
+ }
@@ -0,0 +1,8 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ type CouponPromotionSource = Pick<HttpTypes.StoreCart, "items" | "promotions" | "shipping_methods">;
3
+ /**
4
+ * The promotions shown as a coupon. An automatic promotion that only adjusts
5
+ * shipping (free shipping) is not one: it shows as discounted shipping instead.
6
+ */
7
+ export declare function getCouponPromotions(cart: CouponPromotionSource): HttpTypes.StoreCartPromotion[];
8
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The promotions shown as a coupon. An automatic promotion that only adjusts
3
+ * shipping (free shipping) is not one: it shows as discounted shipping instead.
4
+ */
5
+ export function getCouponPromotions(cart) {
6
+ const itemPromotionIds = new Set((cart.items ?? []).flatMap((item) => item.adjustments ?? []).map((adjustment) => adjustment.promotion_id));
7
+ const shippingPromotionIds = new Set((cart.shipping_methods ?? []).flatMap((method) => method.adjustments ?? []).map((adjustment) => adjustment.promotion_id));
8
+ return cart.promotions.filter((promotion) => !(promotion.is_automatic && shippingPromotionIds.has(promotion.id) && !itemPromotionIds.has(promotion.id)));
9
+ }
@@ -0,0 +1,3 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ export declare function isPaymentSessionOutdated(cart: Pick<HttpTypes.StoreCart, "currency_code" | "total">, session: Pick<HttpTypes.StorePaymentSession, "amount" | "currency_code">): boolean;
3
+ export declare function findPendingPaymentSession(cart: Pick<HttpTypes.StoreCart, "payment_collection">): HttpTypes.StorePaymentSession | undefined;
@@ -0,0 +1,12 @@
1
+ // A session still for the cart's total and currency charges the right amount;
2
+ // re-initiating it would only create a new PaymentIntent and reload Stripe
3
+ // Elements for nothing.
4
+ export function isPaymentSessionOutdated(cart, session) {
5
+ return session.amount !== cart.total || session.currency_code.toLowerCase() !== cart.currency_code.toLowerCase();
6
+ }
7
+ // A "requires_more" session is a 3DS flow already in progress and must not be
8
+ // torn down by an unrelated cart-price change (e.g. a quantity bump) - only a
9
+ // still-pending session is safe to re-initiate.
10
+ export function findPendingPaymentSession(cart) {
11
+ return cart.payment_collection?.payment_sessions?.find((session) => session.status === "pending");
12
+ }
@@ -0,0 +1,27 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ /** Cart metadata key naming the refunded cart a replacement cart was created for. */
3
+ export declare const REPLACED_REFUNDED_CART_METADATA_KEY = "replaced_refunded_cart_id";
4
+ type RefundCheckCart = Pick<HttpTypes.StoreCart, "completed_at" | "payment_collection">;
5
+ type ReplacementSourceCart = Pick<HttpTypes.StoreCart, "billing_address" | "email" | "id" | "items" | "region_id" | "shipping_address">;
6
+ /**
7
+ * Stripe captures on confirmation, so a cart completion failing afterwards
8
+ * makes Medusa refund the payment. It then cannot delete the session of the
9
+ * succeeded PaymentIntent, so the cart stays open with it: every cart change
10
+ * fails ("Could not delete all payment sessions") and the backend refuses to
11
+ * complete it. Such a cart is replaced, never repaired. A payment whose refund
12
+ * failed is still captured and keeps the "Bestellung abschließen" retry.
13
+ */
14
+ export declare function hasRefundedPayment(cart: RefundCheckCart): boolean;
15
+ /**
16
+ * The cart that takes over from a refunded one: same region, items, e-mail
17
+ * and addresses. Coupon codes follow separately (getCustomerCouponCodes); the
18
+ * own-order promotion is applied again when the payment step starts.
19
+ */
20
+ export declare function buildReplacementCart(cart: ReplacementSourceCart): HttpTypes.StoreCreateCart;
21
+ export declare function isRefundReplacementCart(cart: Pick<HttpTypes.StoreCart, "metadata">): boolean;
22
+ /**
23
+ * The codes the customer entered on the refunded cart, to apply again on its
24
+ * replacement. Automatic promotions apply by themselves.
25
+ */
26
+ export declare function getCustomerCouponCodes(cart: Pick<HttpTypes.StoreCart, "promotions">): string[];
27
+ export {};
@@ -0,0 +1,46 @@
1
+ import { toCartAddress } from "../addresses.js";
2
+ /** Cart metadata key naming the refunded cart a replacement cart was created for. */
3
+ export const REPLACED_REFUNDED_CART_METADATA_KEY = "replaced_refunded_cart_id";
4
+ /**
5
+ * Stripe captures on confirmation, so a cart completion failing afterwards
6
+ * makes Medusa refund the payment. It then cannot delete the session of the
7
+ * succeeded PaymentIntent, so the cart stays open with it: every cart change
8
+ * fails ("Could not delete all payment sessions") and the backend refuses to
9
+ * complete it. Such a cart is replaced, never repaired. A payment whose refund
10
+ * failed is still captured and keeps the "Bestellung abschließen" retry.
11
+ */
12
+ export function hasRefundedPayment(cart) {
13
+ return !cart.completed_at && (cart.payment_collection?.refunded_amount ?? 0) > 0;
14
+ }
15
+ function toReplacementAddress(address) {
16
+ if (!address) {
17
+ return undefined;
18
+ }
19
+ // The metadata carries the billing UID (tax-compliance).
20
+ return address.metadata ? { ...toCartAddress(address), metadata: address.metadata } : toCartAddress(address);
21
+ }
22
+ /**
23
+ * The cart that takes over from a refunded one: same region, items, e-mail
24
+ * and addresses. Coupon codes follow separately (getCustomerCouponCodes); the
25
+ * own-order promotion is applied again when the payment step starts.
26
+ */
27
+ export function buildReplacementCart(cart) {
28
+ return {
29
+ billing_address: toReplacementAddress(cart.billing_address),
30
+ email: cart.email ?? undefined,
31
+ items: (cart.items ?? []).flatMap((item) => (item.variant_id ? [{ quantity: item.quantity, variant_id: item.variant_id }] : [])),
32
+ metadata: { [REPLACED_REFUNDED_CART_METADATA_KEY]: cart.id },
33
+ region_id: cart.region_id,
34
+ shipping_address: toReplacementAddress(cart.shipping_address),
35
+ };
36
+ }
37
+ export function isRefundReplacementCart(cart) {
38
+ return typeof cart.metadata?.[REPLACED_REFUNDED_CART_METADATA_KEY] === "string";
39
+ }
40
+ /**
41
+ * The codes the customer entered on the refunded cart, to apply again on its
42
+ * replacement. Automatic promotions apply by themselves.
43
+ */
44
+ export function getCustomerCouponCodes(cart) {
45
+ return cart.promotions.flatMap((promotion) => (promotion.code && !promotion.is_automatic ? [promotion.code] : []));
46
+ }
@@ -0,0 +1,16 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ /** A category with the descendants Medusa returns for `include_descendants_tree`. */
3
+ export type CategoryTreeNode = Pick<HttpTypes.StoreProductCategory, "id" | "handle" | "name"> & {
4
+ category_children?: CategoryTreeNode[] | null;
5
+ };
6
+ /** The category and all its descendants: choosing a parent filter includes its subcategories. */
7
+ export declare function collectCategoryIds(category: CategoryTreeNode): string[];
8
+ /** The node with this handle anywhere in the trees, or null. */
9
+ export declare function findCategoryByHandle(trees: CategoryTreeNode[], handle: string): CategoryTreeNode | null;
10
+ /**
11
+ * Keeps the products that match every selected filter: a product matches one
12
+ * filter when it belongs to the chosen category or one of its descendants.
13
+ * Medusa's `category_id` is an OR list, so two filters at once (e.g. an
14
+ * application area AND an ingredient group) are intersected here.
15
+ */
16
+ export declare function filterProductsByCategories<T extends Pick<HttpTypes.StoreProduct, "categories">>(products: T[], selected: CategoryTreeNode[]): T[];
@@ -0,0 +1,33 @@
1
+ /** The category and all its descendants: choosing a parent filter includes its subcategories. */
2
+ export function collectCategoryIds(category) {
3
+ return [category.id, ...(category.category_children ?? []).flatMap(collectCategoryIds)];
4
+ }
5
+ /** The node with this handle anywhere in the trees, or null. */
6
+ export function findCategoryByHandle(trees, handle) {
7
+ for (const node of trees) {
8
+ if (node.handle === handle) {
9
+ return node;
10
+ }
11
+ const match = findCategoryByHandle(node.category_children ?? [], handle);
12
+ if (match) {
13
+ return match;
14
+ }
15
+ }
16
+ return null;
17
+ }
18
+ /**
19
+ * Keeps the products that match every selected filter: a product matches one
20
+ * filter when it belongs to the chosen category or one of its descendants.
21
+ * Medusa's `category_id` is an OR list, so two filters at once (e.g. an
22
+ * application area AND an ingredient group) are intersected here.
23
+ */
24
+ export function filterProductsByCategories(products, selected) {
25
+ if (selected.length === 0) {
26
+ return products;
27
+ }
28
+ const filters = selected.map((category) => new Set(collectCategoryIds(category)));
29
+ return products.filter((product) => {
30
+ const productCategoryIds = (product.categories ?? []).map((category) => category.id);
31
+ return filters.every((filter) => productCategoryIds.some((id) => filter.has(id)));
32
+ });
33
+ }
@@ -0,0 +1,10 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ type InventorySource = Pick<HttpTypes.StoreProductVariant, "allow_backorder" | "inventory_quantity" | "manage_inventory">;
3
+ /**
4
+ * A variant is out of stock only when all three hold: inventory is managed,
5
+ * backorders are not allowed, and nothing is left. A bare
6
+ * `inventory_quantity <= 0` check wrongly blocks a variant whose inventory is
7
+ * not managed or that may be backordered.
8
+ */
9
+ export declare function isVariantOutOfStock(variant: InventorySource): boolean;
10
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A variant is out of stock only when all three hold: inventory is managed,
3
+ * backorders are not allowed, and nothing is left. A bare
4
+ * `inventory_quantity <= 0` check wrongly blocks a variant whose inventory is
5
+ * not managed or that may be backordered.
6
+ */
7
+ export function isVariantOutOfStock(variant) {
8
+ return variant.manage_inventory === true && variant.allow_backorder !== true && typeof variant.inventory_quantity === "number" && variant.inventory_quantity <= 0;
9
+ }
@@ -0,0 +1,8 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ interface MedusaProductImage {
3
+ alt: string;
4
+ url: string;
5
+ }
6
+ export declare function getMedusaProductImages(product: Pick<HttpTypes.StoreProduct, "images" | "thumbnail" | "title">): MedusaProductImage[];
7
+ export declare function getMedusaProductImage(product: Pick<HttpTypes.StoreProduct, "images" | "thumbnail" | "title">): MedusaProductImage | null;
8
+ export {};
@@ -0,0 +1,13 @@
1
+ export function getMedusaProductImages(product) {
2
+ const imageUrls = new Set();
3
+ return [product.thumbnail, ...(product.images ?? []).map((image) => image.url)].flatMap((url) => {
4
+ if (!url || imageUrls.has(url)) {
5
+ return [];
6
+ }
7
+ imageUrls.add(url);
8
+ return [{ alt: product.title, url }];
9
+ });
10
+ }
11
+ export function getMedusaProductImage(product) {
12
+ return getMedusaProductImages(product)[0] ?? null;
13
+ }
@@ -0,0 +1,8 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ interface ProductPrice {
3
+ amount: number;
4
+ currencyCode: string;
5
+ originalAmount: number | null;
6
+ }
7
+ export declare function getProductPrice(product: Pick<HttpTypes.StoreProduct, "variants">): ProductPrice | null;
8
+ export {};
@@ -0,0 +1,21 @@
1
+ export function getProductPrice(product) {
2
+ const prices = (product.variants ?? []).flatMap((variant) => {
3
+ const calculatedPrice = variant.calculated_price;
4
+ if (typeof calculatedPrice?.calculated_amount !== "number" || typeof calculatedPrice.currency_code !== "string") {
5
+ return [];
6
+ }
7
+ const grossAmount = typeof calculatedPrice.calculated_amount_with_tax === "number" ? calculatedPrice.calculated_amount_with_tax : calculatedPrice.calculated_amount;
8
+ const grossOriginalAmount = typeof calculatedPrice.original_amount_with_tax === "number" ? calculatedPrice.original_amount_with_tax : calculatedPrice.original_amount;
9
+ return [
10
+ {
11
+ amount: grossAmount,
12
+ currencyCode: calculatedPrice.currency_code,
13
+ originalAmount: typeof grossOriginalAmount === "number" ? grossOriginalAmount : null,
14
+ },
15
+ ];
16
+ });
17
+ if (prices.length === 0) {
18
+ return null;
19
+ }
20
+ return prices.reduce((lowestPrice, price) => (price.amount < lowestPrice.amount ? price : lowestPrice));
21
+ }
@@ -0,0 +1,7 @@
1
+ import type { HttpTypes } from "@medusajs/types";
2
+ /**
3
+ * The variant the product page shows and sells: the one from `?v_id`, or the
4
+ * first variant when the id is missing or unknown. Shared by the price and the
5
+ * cart button so both always refer to the same variant.
6
+ */
7
+ export declare function resolveSelectedVariant(variants: HttpTypes.StoreProductVariant[] | null | undefined, selectedVariantId: string | undefined): HttpTypes.StoreProductVariant | undefined;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The variant the product page shows and sells: the one from `?v_id`, or the
3
+ * first variant when the id is missing or unknown. Shared by the price and the
4
+ * cart button so both always refer to the same variant.
5
+ */
6
+ export function resolveSelectedVariant(variants, selectedVariantId) {
7
+ const list = variants ?? [];
8
+ return list.find((variant) => variant.id === selectedVariantId) ?? list[0];
9
+ }
@@ -0,0 +1,41 @@
1
+ export type AccountOfferStep = "ask" | "code" | "declined" | "done";
2
+ export interface AccountOfferState {
3
+ /** Normalized address the step belongs to ("" while the cart has none). */
4
+ email: string;
5
+ step: AccountOfferStep;
6
+ }
7
+ export type AccountOfferAction = {
8
+ type: "accepted";
9
+ } | {
10
+ type: "cancelled";
11
+ } | {
12
+ type: "declined";
13
+ } | {
14
+ email: string;
15
+ type: "emailChanged";
16
+ } | {
17
+ type: "reconsidered";
18
+ } | {
19
+ type: "signedIn";
20
+ };
21
+ export declare function normalizeOfferEmail(email: string | null | undefined): string;
22
+ export declare function createAccountOfferState(email: string | null | undefined): AccountOfferState;
23
+ export declare function accountOfferReducer(state: AccountOfferState, action: AccountOfferAction): AccountOfferState;
24
+ export interface AccountOfferVisibilityInput {
25
+ /** The shop offers the e-mail code login (customer_auth.email_otp). */
26
+ codeLoginEnabled: boolean;
27
+ /** The e-mail on the cart. */
28
+ email: string | null | undefined;
29
+ /**
30
+ * The e-mail field's current value. While it differs from the cart (typed
31
+ * but not yet written on blur), the offer hides, so "yes" can never mail a
32
+ * code to the previous address.
33
+ */
34
+ fieldEmail: string;
35
+ /** The customer query settled and found nobody logged in. */
36
+ isGuest: boolean;
37
+ /** The checkout currently demands a login for the e-mail (the login prompt replaces the form). */
38
+ isLoginRequired: boolean;
39
+ step: AccountOfferStep;
40
+ }
41
+ export declare function isAccountOfferVisible({ codeLoginEnabled, email, fieldEmail, isGuest, isLoginRequired, step }: AccountOfferVisibilityInput): boolean;