medusa-profile-shipping 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.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # medusa-profile-shipping
2
+
3
+ Profile-grouped shipping selection for Medusa v2 storefronts.
4
+
5
+ Medusa requires **one shipping method per shipping profile** in the cart:
6
+ core validates coverage at "Place Order", and its add-shipping-method
7
+ workflow replaces same-profile methods while keeping the rest. The stock
8
+ Next.js starter renders a single flat option list, so a mixed cart (a
9
+ regulated item on an "FFL" profile plus a t-shirt on the default profile)
10
+ can never be completed through it — the buyer picks one method, and
11
+ completion rejects with *"the cart items require shipping profiles that are
12
+ not satisfied"*.
13
+
14
+ This package turns that into a per-profile selection: group the cart's items
15
+ by shipping profile, show each group the options that can actually fulfill
16
+ it, and require one pick per group before continuing to payment.
17
+
18
+ Zero runtime dependencies. Structurally typed (no `@medusajs/*` version
19
+ coupling): pass any objects shaped like a Medusa cart and its shipping
20
+ options. React is an optional peer, only for the `/react` hook.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install medusa-profile-shipping
26
+ ```
27
+
28
+ ## Data prerequisites
29
+
30
+ Fetch with these fields so the grouping has data (both are plain Medusa
31
+ store-API field selections):
32
+
33
+ ```ts
34
+ // cart:
35
+ "items.product.shipping_profile.id,items.product.shipping_profile.name,+shipping_methods.shipping_option_id"
36
+ // shipping options:
37
+ "+shipping_profile_id"
38
+ ```
39
+
40
+ ## Helpers (framework-agnostic)
41
+
42
+ ```ts
43
+ import {
44
+ groupCartByShippingProfile,
45
+ optionsForProfileGroup,
46
+ selectionsFromCart,
47
+ allGroupsCovered,
48
+ groupKey,
49
+ } from "medusa-profile-shipping"
50
+
51
+ const groups = groupCartByShippingProfile(cart)
52
+ // [{ profileId: "sp_nfa", profileName: "NFA", itemTitles: ["Suppressor"] },
53
+ // { profileId: "sp_default", profileName: "Other", itemTitles: ["Shirt"] }]
54
+
55
+ const nfaOptions = optionsForProfileGroup(shippingOptions, groups[0])
56
+ const selections = selectionsFromCart(cart, shippingOptions)
57
+ const readyForPayment = allGroupsCovered(groups, selections)
58
+ ```
59
+
60
+ Missing data fails open: items without profile data form one group that sees
61
+ every option, and option lists without `shipping_profile_id` are never
62
+ filtered. Medusa's completion validation remains the backstop.
63
+
64
+ ## React hook (headless)
65
+
66
+ ```tsx
67
+ import { useProfileShipping } from "medusa-profile-shipping/react"
68
+
69
+ const { groups, optionsFor, selections, select, covered, saving, error, groupKey } =
70
+ useProfileShipping({
71
+ cart,
72
+ options: shippingOptions,
73
+ // your server action / API call; Medusa keeps other profiles' methods
74
+ setShippingMethod: (optionId) =>
75
+ sdk.store.cart.addShippingMethod(cart.id, { option_id: optionId }),
76
+ })
77
+
78
+ return (
79
+ <>
80
+ {groups.map((group) => (
81
+ <fieldset key={groupKey(group)}>
82
+ {groups.length > 1 && (
83
+ <legend>
84
+ {group.profileName} — {group.itemTitles.join(", ")}
85
+ </legend>
86
+ )}
87
+ {optionsFor(group).map((option) => (
88
+ <label key={option.id}>
89
+ <input
90
+ type="radio"
91
+ checked={selections[groupKey(group)] === option.id}
92
+ onChange={() => select(group, option.id)}
93
+ />
94
+ {option.name}
95
+ </label>
96
+ ))}
97
+ </fieldset>
98
+ ))}
99
+ <button disabled={!covered || saving}>Continue to payment</button>
100
+ </>
101
+ )
102
+ ```
103
+
104
+ `select()` is optimistic with rollback, `covered` gates the continue button,
105
+ and single-profile carts produce one group — render it without the legend
106
+ and the step looks identical to stock.
107
+
108
+ ## Why this isn't a Medusa (backend) plugin
109
+
110
+ There is nothing to install server-side: Medusa core already validates
111
+ profile coverage at completion and manages one-method-per-profile on the
112
+ cart. The entire gap is storefront UI, which backend plugins cannot ship.
113
+ This package is the reusable piece of that UI logic.
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,54 @@
1
+ // src/index.ts
2
+ var NO_PROFILE_KEY = "__no_profile__";
3
+ function groupKey(group) {
4
+ return group.profileId ?? NO_PROFILE_KEY;
5
+ }
6
+ function groupCartByShippingProfile(cart) {
7
+ const groups = /* @__PURE__ */ new Map();
8
+ for (const item of cart.items ?? []) {
9
+ const profile = item.product?.shipping_profile;
10
+ const key = profile?.id ?? NO_PROFILE_KEY;
11
+ const title = item.product_title ?? item.title ?? "Item";
12
+ const existing = groups.get(key);
13
+ if (existing) {
14
+ if (!existing.itemTitles.includes(title)) existing.itemTitles.push(title);
15
+ } else {
16
+ groups.set(key, {
17
+ profileId: profile?.id ?? null,
18
+ profileName: profile?.name ?? "Other items",
19
+ itemTitles: [title]
20
+ });
21
+ }
22
+ }
23
+ return Array.from(groups.values());
24
+ }
25
+ function optionsForProfileGroup(options, group) {
26
+ if (!group.profileId) return options;
27
+ if (!options.some((o) => o.shipping_profile_id)) return options;
28
+ return options.filter((o) => o.shipping_profile_id === group.profileId);
29
+ }
30
+ function selectionsFromCart(cart, options) {
31
+ const optionProfile = new Map(
32
+ options.map((o) => [o.id, o.shipping_profile_id ?? null])
33
+ );
34
+ const out = {};
35
+ for (const method of cart.shipping_methods ?? []) {
36
+ const optionId = method.shipping_option_id;
37
+ if (!optionId) continue;
38
+ const profileId = optionProfile.get(optionId);
39
+ out[profileId ?? NO_PROFILE_KEY] = optionId;
40
+ }
41
+ return out;
42
+ }
43
+ function allGroupsCovered(groups, selections) {
44
+ return groups.every((g) => !!selections[groupKey(g)]);
45
+ }
46
+
47
+ export {
48
+ NO_PROFILE_KEY,
49
+ groupKey,
50
+ groupCartByShippingProfile,
51
+ optionsForProfileGroup,
52
+ selectionsFromCart,
53
+ allGroupsCovered
54
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * medusa-profile-shipping — profile-grouped shipping selection for Medusa v2
3
+ * storefronts.
4
+ *
5
+ * Medusa requires one shipping method per shipping profile present in the
6
+ * cart: core validates coverage at completion, and its add-shipping-method
7
+ * workflow replaces same-profile methods while keeping the rest. Stock
8
+ * storefronts render a single flat option list, so mixed-profile carts (a
9
+ * regulated item plus an accessory, say) cannot be completed through them.
10
+ *
11
+ * These helpers are structurally typed and dependency-free: pass any objects
12
+ * shaped like a Medusa cart and its shipping options (HttpTypes.StoreCart and
13
+ * StoreCartShippingOption satisfy them). Fetch shipping options with
14
+ * `fields=+shipping_profile_id` and the cart with
15
+ * `items.product.shipping_profile.id` / `.name` so the grouping has data.
16
+ */
17
+ type ProfileShippingCartItem = {
18
+ title?: string | null;
19
+ product_title?: string | null;
20
+ product?: {
21
+ shipping_profile?: {
22
+ id?: string | null;
23
+ name?: string | null;
24
+ } | null;
25
+ } | null;
26
+ };
27
+ type ProfileShippingCart = {
28
+ items?: ProfileShippingCartItem[] | null;
29
+ shipping_methods?: {
30
+ shipping_option_id?: string | null;
31
+ }[] | null;
32
+ };
33
+ type ProfileShippingOption = {
34
+ id: string;
35
+ shipping_profile_id?: string | null;
36
+ };
37
+ type ShippingProfileGroup = {
38
+ /** null when the cart items carried no profile data (options unfiltered). */
39
+ profileId: string | null;
40
+ profileName: string;
41
+ itemTitles: string[];
42
+ };
43
+ /** Key used in selection maps for items without profile data. */
44
+ declare const NO_PROFILE_KEY = "__no_profile__";
45
+ /** Selection-map key for a group. */
46
+ declare function groupKey(group: ShippingProfileGroup): string;
47
+ /** Unique shipping-profile groups across the cart's items, stable order. */
48
+ declare function groupCartByShippingProfile(cart: ProfileShippingCart): ShippingProfileGroup[];
49
+ /**
50
+ * The options that can fulfill a group. Unfiltered when the group or the
51
+ * option list carries no profile data — never hide everything on missing
52
+ * fields; Medusa's completion validation remains the backstop.
53
+ */
54
+ declare function optionsForProfileGroup<T extends ProfileShippingOption>(options: T[], group: ShippingProfileGroup): T[];
55
+ /** profileId (or NO_PROFILE_KEY) -> selected option id, from saved methods. */
56
+ declare function selectionsFromCart(cart: ProfileShippingCart, options: ProfileShippingOption[]): Record<string, string>;
57
+ /** True when every profile group has a selected method. */
58
+ declare function allGroupsCovered(groups: ShippingProfileGroup[], selections: Record<string, string>): boolean;
59
+
60
+ export { NO_PROFILE_KEY, type ProfileShippingCart, type ProfileShippingCartItem, type ProfileShippingOption, type ShippingProfileGroup, allGroupsCovered, groupCartByShippingProfile, groupKey, optionsForProfileGroup, selectionsFromCart };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * medusa-profile-shipping — profile-grouped shipping selection for Medusa v2
3
+ * storefronts.
4
+ *
5
+ * Medusa requires one shipping method per shipping profile present in the
6
+ * cart: core validates coverage at completion, and its add-shipping-method
7
+ * workflow replaces same-profile methods while keeping the rest. Stock
8
+ * storefronts render a single flat option list, so mixed-profile carts (a
9
+ * regulated item plus an accessory, say) cannot be completed through them.
10
+ *
11
+ * These helpers are structurally typed and dependency-free: pass any objects
12
+ * shaped like a Medusa cart and its shipping options (HttpTypes.StoreCart and
13
+ * StoreCartShippingOption satisfy them). Fetch shipping options with
14
+ * `fields=+shipping_profile_id` and the cart with
15
+ * `items.product.shipping_profile.id` / `.name` so the grouping has data.
16
+ */
17
+ type ProfileShippingCartItem = {
18
+ title?: string | null;
19
+ product_title?: string | null;
20
+ product?: {
21
+ shipping_profile?: {
22
+ id?: string | null;
23
+ name?: string | null;
24
+ } | null;
25
+ } | null;
26
+ };
27
+ type ProfileShippingCart = {
28
+ items?: ProfileShippingCartItem[] | null;
29
+ shipping_methods?: {
30
+ shipping_option_id?: string | null;
31
+ }[] | null;
32
+ };
33
+ type ProfileShippingOption = {
34
+ id: string;
35
+ shipping_profile_id?: string | null;
36
+ };
37
+ type ShippingProfileGroup = {
38
+ /** null when the cart items carried no profile data (options unfiltered). */
39
+ profileId: string | null;
40
+ profileName: string;
41
+ itemTitles: string[];
42
+ };
43
+ /** Key used in selection maps for items without profile data. */
44
+ declare const NO_PROFILE_KEY = "__no_profile__";
45
+ /** Selection-map key for a group. */
46
+ declare function groupKey(group: ShippingProfileGroup): string;
47
+ /** Unique shipping-profile groups across the cart's items, stable order. */
48
+ declare function groupCartByShippingProfile(cart: ProfileShippingCart): ShippingProfileGroup[];
49
+ /**
50
+ * The options that can fulfill a group. Unfiltered when the group or the
51
+ * option list carries no profile data — never hide everything on missing
52
+ * fields; Medusa's completion validation remains the backstop.
53
+ */
54
+ declare function optionsForProfileGroup<T extends ProfileShippingOption>(options: T[], group: ShippingProfileGroup): T[];
55
+ /** profileId (or NO_PROFILE_KEY) -> selected option id, from saved methods. */
56
+ declare function selectionsFromCart(cart: ProfileShippingCart, options: ProfileShippingOption[]): Record<string, string>;
57
+ /** True when every profile group has a selected method. */
58
+ declare function allGroupsCovered(groups: ShippingProfileGroup[], selections: Record<string, string>): boolean;
59
+
60
+ export { NO_PROFILE_KEY, type ProfileShippingCart, type ProfileShippingCartItem, type ProfileShippingOption, type ShippingProfileGroup, allGroupsCovered, groupCartByShippingProfile, groupKey, optionsForProfileGroup, selectionsFromCart };
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ NO_PROFILE_KEY: () => NO_PROFILE_KEY,
24
+ allGroupsCovered: () => allGroupsCovered,
25
+ groupCartByShippingProfile: () => groupCartByShippingProfile,
26
+ groupKey: () => groupKey,
27
+ optionsForProfileGroup: () => optionsForProfileGroup,
28
+ selectionsFromCart: () => selectionsFromCart
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var NO_PROFILE_KEY = "__no_profile__";
32
+ function groupKey(group) {
33
+ return group.profileId ?? NO_PROFILE_KEY;
34
+ }
35
+ function groupCartByShippingProfile(cart) {
36
+ const groups = /* @__PURE__ */ new Map();
37
+ for (const item of cart.items ?? []) {
38
+ const profile = item.product?.shipping_profile;
39
+ const key = profile?.id ?? NO_PROFILE_KEY;
40
+ const title = item.product_title ?? item.title ?? "Item";
41
+ const existing = groups.get(key);
42
+ if (existing) {
43
+ if (!existing.itemTitles.includes(title)) existing.itemTitles.push(title);
44
+ } else {
45
+ groups.set(key, {
46
+ profileId: profile?.id ?? null,
47
+ profileName: profile?.name ?? "Other items",
48
+ itemTitles: [title]
49
+ });
50
+ }
51
+ }
52
+ return Array.from(groups.values());
53
+ }
54
+ function optionsForProfileGroup(options, group) {
55
+ if (!group.profileId) return options;
56
+ if (!options.some((o) => o.shipping_profile_id)) return options;
57
+ return options.filter((o) => o.shipping_profile_id === group.profileId);
58
+ }
59
+ function selectionsFromCart(cart, options) {
60
+ const optionProfile = new Map(
61
+ options.map((o) => [o.id, o.shipping_profile_id ?? null])
62
+ );
63
+ const out = {};
64
+ for (const method of cart.shipping_methods ?? []) {
65
+ const optionId = method.shipping_option_id;
66
+ if (!optionId) continue;
67
+ const profileId = optionProfile.get(optionId);
68
+ out[profileId ?? NO_PROFILE_KEY] = optionId;
69
+ }
70
+ return out;
71
+ }
72
+ function allGroupsCovered(groups, selections) {
73
+ return groups.every((g) => !!selections[groupKey(g)]);
74
+ }
75
+ // Annotate the CommonJS export names for ESM import in node:
76
+ 0 && (module.exports = {
77
+ NO_PROFILE_KEY,
78
+ allGroupsCovered,
79
+ groupCartByShippingProfile,
80
+ groupKey,
81
+ optionsForProfileGroup,
82
+ selectionsFromCart
83
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,16 @@
1
+ import {
2
+ NO_PROFILE_KEY,
3
+ allGroupsCovered,
4
+ groupCartByShippingProfile,
5
+ groupKey,
6
+ optionsForProfileGroup,
7
+ selectionsFromCart
8
+ } from "./chunk-QNKJOATD.mjs";
9
+ export {
10
+ NO_PROFILE_KEY,
11
+ allGroupsCovered,
12
+ groupCartByShippingProfile,
13
+ groupKey,
14
+ optionsForProfileGroup,
15
+ selectionsFromCart
16
+ };
@@ -0,0 +1,25 @@
1
+ import { ProfileShippingOption, ProfileShippingCart, ShippingProfileGroup, groupKey } from './index.mjs';
2
+
3
+ type UseProfileShippingArgs<TOption extends ProfileShippingOption> = {
4
+ cart: ProfileShippingCart;
5
+ options: TOption[];
6
+ /** Persist one selection (Medusa replaces same-profile methods, keeps others). */
7
+ setShippingMethod: (optionId: string) => Promise<unknown>;
8
+ };
9
+ type UseProfileShippingResult<TOption extends ProfileShippingOption> = {
10
+ groups: ShippingProfileGroup[];
11
+ /** Options that can fulfill the given group. */
12
+ optionsFor: (group: ShippingProfileGroup) => TOption[];
13
+ /** group key -> selected option id */
14
+ selections: Record<string, string>;
15
+ /** Select an option for a group; optimistic with rollback on failure. */
16
+ select: (group: ShippingProfileGroup, optionId: string) => Promise<void>;
17
+ /** Every group has a method — safe to continue to payment. */
18
+ covered: boolean;
19
+ saving: boolean;
20
+ error: string | null;
21
+ groupKey: typeof groupKey;
22
+ };
23
+ declare function useProfileShipping<TOption extends ProfileShippingOption>({ cart, options, setShippingMethod, }: UseProfileShippingArgs<TOption>): UseProfileShippingResult<TOption>;
24
+
25
+ export { type UseProfileShippingArgs, type UseProfileShippingResult, useProfileShipping };
@@ -0,0 +1,25 @@
1
+ import { ProfileShippingOption, ProfileShippingCart, ShippingProfileGroup, groupKey } from './index.js';
2
+
3
+ type UseProfileShippingArgs<TOption extends ProfileShippingOption> = {
4
+ cart: ProfileShippingCart;
5
+ options: TOption[];
6
+ /** Persist one selection (Medusa replaces same-profile methods, keeps others). */
7
+ setShippingMethod: (optionId: string) => Promise<unknown>;
8
+ };
9
+ type UseProfileShippingResult<TOption extends ProfileShippingOption> = {
10
+ groups: ShippingProfileGroup[];
11
+ /** Options that can fulfill the given group. */
12
+ optionsFor: (group: ShippingProfileGroup) => TOption[];
13
+ /** group key -> selected option id */
14
+ selections: Record<string, string>;
15
+ /** Select an option for a group; optimistic with rollback on failure. */
16
+ select: (group: ShippingProfileGroup, optionId: string) => Promise<void>;
17
+ /** Every group has a method — safe to continue to payment. */
18
+ covered: boolean;
19
+ saving: boolean;
20
+ error: string | null;
21
+ groupKey: typeof groupKey;
22
+ };
23
+ declare function useProfileShipping<TOption extends ProfileShippingOption>({ cart, options, setShippingMethod, }: UseProfileShippingArgs<TOption>): UseProfileShippingResult<TOption>;
24
+
25
+ export { type UseProfileShippingArgs, type UseProfileShippingResult, useProfileShipping };
package/dist/react.js ADDED
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ useProfileShipping: () => useProfileShipping
24
+ });
25
+ module.exports = __toCommonJS(react_exports);
26
+ var import_react = require("react");
27
+
28
+ // src/index.ts
29
+ var NO_PROFILE_KEY = "__no_profile__";
30
+ function groupKey(group) {
31
+ return group.profileId ?? NO_PROFILE_KEY;
32
+ }
33
+ function groupCartByShippingProfile(cart) {
34
+ const groups = /* @__PURE__ */ new Map();
35
+ for (const item of cart.items ?? []) {
36
+ const profile = item.product?.shipping_profile;
37
+ const key = profile?.id ?? NO_PROFILE_KEY;
38
+ const title = item.product_title ?? item.title ?? "Item";
39
+ const existing = groups.get(key);
40
+ if (existing) {
41
+ if (!existing.itemTitles.includes(title)) existing.itemTitles.push(title);
42
+ } else {
43
+ groups.set(key, {
44
+ profileId: profile?.id ?? null,
45
+ profileName: profile?.name ?? "Other items",
46
+ itemTitles: [title]
47
+ });
48
+ }
49
+ }
50
+ return Array.from(groups.values());
51
+ }
52
+ function optionsForProfileGroup(options, group) {
53
+ if (!group.profileId) return options;
54
+ if (!options.some((o) => o.shipping_profile_id)) return options;
55
+ return options.filter((o) => o.shipping_profile_id === group.profileId);
56
+ }
57
+ function selectionsFromCart(cart, options) {
58
+ const optionProfile = new Map(
59
+ options.map((o) => [o.id, o.shipping_profile_id ?? null])
60
+ );
61
+ const out = {};
62
+ for (const method of cart.shipping_methods ?? []) {
63
+ const optionId = method.shipping_option_id;
64
+ if (!optionId) continue;
65
+ const profileId = optionProfile.get(optionId);
66
+ out[profileId ?? NO_PROFILE_KEY] = optionId;
67
+ }
68
+ return out;
69
+ }
70
+ function allGroupsCovered(groups, selections) {
71
+ return groups.every((g) => !!selections[groupKey(g)]);
72
+ }
73
+
74
+ // src/react.ts
75
+ function useProfileShipping({
76
+ cart,
77
+ options,
78
+ setShippingMethod
79
+ }) {
80
+ const groups = (0, import_react.useMemo)(() => groupCartByShippingProfile(cart), [cart]);
81
+ const [selections, setSelections] = (0, import_react.useState)(
82
+ () => selectionsFromCart(cart, options)
83
+ );
84
+ const [saving, setSaving] = (0, import_react.useState)(false);
85
+ const [error, setError] = (0, import_react.useState)(null);
86
+ (0, import_react.useEffect)(() => {
87
+ setSelections(selectionsFromCart(cart, options));
88
+ }, [cart.shipping_methods, options]);
89
+ const optionsFor = (0, import_react.useCallback)(
90
+ (group) => optionsForProfileGroup(options, group),
91
+ [options]
92
+ );
93
+ const select = (0, import_react.useCallback)(
94
+ async (group, optionId) => {
95
+ const key = groupKey(group);
96
+ const previous = selections[key];
97
+ setError(null);
98
+ setSaving(true);
99
+ setSelections((prev) => ({ ...prev, [key]: optionId }));
100
+ try {
101
+ await setShippingMethod(optionId);
102
+ } catch (e) {
103
+ setSelections((prev) => {
104
+ const next = { ...prev };
105
+ if (previous) next[key] = previous;
106
+ else delete next[key];
107
+ return next;
108
+ });
109
+ setError(e instanceof Error ? e.message : String(e));
110
+ } finally {
111
+ setSaving(false);
112
+ }
113
+ },
114
+ [selections, setShippingMethod]
115
+ );
116
+ return {
117
+ groups,
118
+ optionsFor,
119
+ selections,
120
+ select,
121
+ covered: allGroupsCovered(groups, selections),
122
+ saving,
123
+ error,
124
+ groupKey
125
+ };
126
+ }
127
+ // Annotate the CommonJS export names for ESM import in node:
128
+ 0 && (module.exports = {
129
+ useProfileShipping
130
+ });
package/dist/react.mjs ADDED
@@ -0,0 +1,65 @@
1
+ import {
2
+ allGroupsCovered,
3
+ groupCartByShippingProfile,
4
+ groupKey,
5
+ optionsForProfileGroup,
6
+ selectionsFromCart
7
+ } from "./chunk-QNKJOATD.mjs";
8
+
9
+ // src/react.ts
10
+ import { useCallback, useEffect, useMemo, useState } from "react";
11
+ function useProfileShipping({
12
+ cart,
13
+ options,
14
+ setShippingMethod
15
+ }) {
16
+ const groups = useMemo(() => groupCartByShippingProfile(cart), [cart]);
17
+ const [selections, setSelections] = useState(
18
+ () => selectionsFromCart(cart, options)
19
+ );
20
+ const [saving, setSaving] = useState(false);
21
+ const [error, setError] = useState(null);
22
+ useEffect(() => {
23
+ setSelections(selectionsFromCart(cart, options));
24
+ }, [cart.shipping_methods, options]);
25
+ const optionsFor = useCallback(
26
+ (group) => optionsForProfileGroup(options, group),
27
+ [options]
28
+ );
29
+ const select = useCallback(
30
+ async (group, optionId) => {
31
+ const key = groupKey(group);
32
+ const previous = selections[key];
33
+ setError(null);
34
+ setSaving(true);
35
+ setSelections((prev) => ({ ...prev, [key]: optionId }));
36
+ try {
37
+ await setShippingMethod(optionId);
38
+ } catch (e) {
39
+ setSelections((prev) => {
40
+ const next = { ...prev };
41
+ if (previous) next[key] = previous;
42
+ else delete next[key];
43
+ return next;
44
+ });
45
+ setError(e instanceof Error ? e.message : String(e));
46
+ } finally {
47
+ setSaving(false);
48
+ }
49
+ },
50
+ [selections, setShippingMethod]
51
+ );
52
+ return {
53
+ groups,
54
+ optionsFor,
55
+ selections,
56
+ select,
57
+ covered: allGroupsCovered(groups, selections),
58
+ saving,
59
+ error,
60
+ groupKey
61
+ };
62
+ }
63
+ export {
64
+ useProfileShipping
65
+ };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "medusa-profile-shipping",
3
+ "version": "0.1.0",
4
+ "description": "Profile-grouped shipping selection for Medusa v2 storefronts — one shipping method per shipping profile, so mixed carts (regulated + regular items) can actually check out",
5
+ "license": "MIT",
6
+ "author": "Kael Broersma",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Kaelbroersma/medusa-profile-shipping.git"
10
+ },
11
+ "keywords": [
12
+ "medusa",
13
+ "medusa-v2",
14
+ "medusajs",
15
+ "shipping",
16
+ "shipping-profile",
17
+ "checkout",
18
+ "storefront",
19
+ "nextjs"
20
+ ],
21
+ "sideEffects": false,
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "main": "./dist/index.js",
27
+ "module": "./dist/index.mjs",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ "./package.json": "./package.json",
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.mjs",
34
+ "require": "./dist/index.js",
35
+ "default": "./dist/index.mjs"
36
+ },
37
+ "./react": {
38
+ "types": "./dist/react.d.ts",
39
+ "import": "./dist/react.mjs",
40
+ "require": "./dist/react.js",
41
+ "default": "./dist/react.mjs"
42
+ }
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "test": "vitest run",
47
+ "typecheck": "tsc --noEmit",
48
+ "prepublishOnly": "npm run build && npm test"
49
+ },
50
+ "peerDependencies": {
51
+ "react": ">=18"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "react": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@types/react": "^18.3.2",
60
+ "react": "^18.2.0",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.6.3",
63
+ "vitest": "^2.0.0"
64
+ },
65
+ "engines": {
66
+ "node": ">=18"
67
+ }
68
+ }