medusa-profile-shipping
v0.1.2
Published
Profile-grouped shipping selection for Medusa v2 storefronts — one shipping method per shipping profile, so mixed carts (regulated + regular items) can actually check out
Maintainers
Readme
medusa-profile-shipping
Profile-grouped shipping selection for Medusa v2 storefronts.
Medusa requires one shipping method per shipping profile in the cart: core validates coverage at "Place Order", and its add-shipping-method workflow replaces same-profile methods while keeping the rest. The stock Next.js starter renders a single flat option list, so a mixed cart (a regulated item on an "FFL" profile plus a t-shirt on the default profile) can never be completed through it — the buyer picks one method, and completion rejects with "the cart items require shipping profiles that are not satisfied".
This package turns that into a per-profile selection: group the cart's items by shipping profile, show each group the options that can actually fulfill it, and require one pick per group before continuing to payment.
Zero runtime dependencies. Structurally typed (no @medusajs/* version
coupling): pass any objects shaped like a Medusa cart and its shipping
options. React is an optional peer, only for the /react hook.
Install
npm install medusa-profile-shippingData prerequisites
Fetch with these fields so the grouping has data (both are plain Medusa store-API field selections):
// cart:
"items.product.shipping_profile.id,items.product.shipping_profile.name,+shipping_methods.shipping_option_id"
// shipping options:
"+shipping_profile_id"Helpers (framework-agnostic)
import {
groupCartByShippingProfile,
optionsForProfileGroup,
selectionsFromCart,
allGroupsCovered,
groupKey,
} from "medusa-profile-shipping"
const groups = groupCartByShippingProfile(cart)
// [{ profileId: "sp_nfa", profileName: "NFA", itemTitles: ["Suppressor"] },
// { profileId: "sp_default", profileName: "Other", itemTitles: ["Shirt"] }]
const nfaOptions = optionsForProfileGroup(shippingOptions, groups[0])
const selections = selectionsFromCart(cart, shippingOptions)
const readyForPayment = allGroupsCovered(groups, selections)Missing data fails open: items without profile data form one group that sees
every option, and option lists without shipping_profile_id are never
filtered. Medusa's completion validation remains the backstop.
React hook (headless)
import { useProfileShipping } from "medusa-profile-shipping/react"
const { groups, optionsFor, selections, select, covered, saving, error, groupKey } =
useProfileShipping({
cart,
options: shippingOptions,
// your server action / API call; Medusa keeps other profiles' methods
setShippingMethod: (optionId) =>
sdk.store.cart.addShippingMethod(cart.id, { option_id: optionId }),
})
return (
<>
{groups.map((group) => (
<fieldset key={groupKey(group)}>
{groups.length > 1 && (
<legend>
{group.profileName} — {group.itemTitles.join(", ")}
</legend>
)}
{optionsFor(group).map((option) => (
<label key={option.id}>
<input
type="radio"
checked={selections[groupKey(group)] === option.id}
onChange={() => select(group, option.id)}
/>
{option.name}
</label>
))}
</fieldset>
))}
<button disabled={!covered || saving}>Continue to payment</button>
</>
)select() is optimistic with rollback, covered gates the continue button,
and single-profile carts produce one group — render it without the legend
and the step looks identical to stock.
Why this isn't a Medusa (backend) plugin
There is nothing to install server-side: Medusa core already validates profile coverage at completion and manages one-method-per-profile on the cart. The entire gap is storefront UI, which backend plugins cannot ship. This package is the reusable piece of that UI logic.
License
MIT
