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 +117 -0
- package/dist/chunk-QNKJOATD.mjs +54 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +83 -0
- package/dist/index.mjs +16 -0
- package/dist/react.d.mts +25 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.js +130 -0
- package/dist/react.mjs +65 -0
- package/package.json +68 -0
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
|
+
};
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/react.d.mts
ADDED
|
@@ -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 };
|
package/dist/react.d.ts
ADDED
|
@@ -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
|
+
}
|