Redeem in Cart on Hydrogen (headless)
Let customers spend points right in your Hydrogen cart drawer or cart page, with the same design you set up for the Redeem in Cart block in Joy admin.
Requires @joy-loyalty/hydrogen 0.3.0 or later.
Overview
<JoyRedeemCart/> is the headless version of the Redeem in Cart block. Mount it inside your cart drawer or cart page and customers get:
- Their points balance and the rewards they can redeem — fixed and dynamic (slider) rewards.
- One-click apply — the reward's discount code is added to the cart automatically.
- An applied rewards list — customers can remove a reward from the cart and get their points back, when Enable customer self-revoke coupon is on (see Behavior settings).
- Helpful hints — which rewards can be combined, and which reward gives the best value.
- An earn-back line — how many points this order will earn.
- A guest state — guests see the earn-back line and a Sign in link, unless you hide the block for guests.
Design and texts come from Joy admin, so you don't style the block in code.
Before you start
Complete Install manually first. You need:
- The
JOY_API_URL,JOY_SHOP_DOMAINandJOY_API_SECRETenvironment variables. - The
/api/joy-tokenroute. - The root loader returning
getJoyInitialData()— the samejoydata<JoyWidget/>uses.
Install with an AI agent (recommended)
Let your AI coding agent (Claude Code, Cursor, Codex and others) add the block for you. The Joy skill walks it through the same steps as Add the block to your cart below, so it adapts the code to your cart drawer and checks the result with you. The skill tells the agent not to touch your headless secret and not to deploy — still review the changes before you commit.
1. Add the Joy skills to your Hydrogen project. Run this in your project root:
npx skills add tinhnv2001/joy-loyalty-hydrogen-skillsThis installs joy-headless-redeem-cart next to joy-headless-install. Added the Joy skills before? Run the same command again — npx skills update only refreshes skills you already have and won't add the new one.
2. Paste this prompt into your agent:
Use the joy-headless-redeem-cart skill to add the Joy Redeem in Cart block to my cart drawer.
Use a dedicated cart fetcher, make sure my cart action returns the cart, and merge the Joy cart
fragment fields that match my Storefront API version. Do not deploy anything.
When you are done, walk me through the verification checklist.Want the block on the /cart page too? Add "and on the cart page" to the prompt.
3. Review the changes. You should see:
package.json—@joy-loyalty/hydrogenat 0.3.0 or later.- A new cart component (for example
app/components/cart/joy-redeem-cart.tsx) rendered in your cart drawer. - Your cart fragment (for example
app/lib/fragments.ts) with the Joy fields added. - Your cart route only if it didn't return the cart yet.
Then run the Verify checklist on your preview store.
Joy isn't on your Hydrogen storefront yet? Do Install with an AI agent first. The cart block needs the token route and root loader that install sets up.
Add the block to your cart
1. Mount the component
Create a small component and render it inside your cart drawer:
import {JoyRedeemCart} from '@joy-loyalty/hydrogen';
import {useFetcher, useRevalidator, useRouteLoaderData} from 'react-router';
export function JoyCartRedeem({cart}: {cart: any}) {
const data = useRouteLoaderData('root') as any;
// Dedicated key — don't share the fetcher your own cart forms use.
const cartFetcher = useFetcher({key: 'joy-redeem-cart'});
const {revalidate} = useRevalidator();
if (!data?.joyApiUrl || !data.joyShopDomain) return null;
return (
<JoyRedeemCart
apiUrl={data.joyApiUrl}
shopDomain={data.joyShopDomain}
initialData={data.joy ?? null}
cart={cart}
cartFetcher={cartFetcher}
cartRevalidate={revalidate}
cartRoute="/cart"
/>
);
}- Give
useFetcherits own key (for examplejoy-redeem-cart) so the block never shares state with your drawer's cart forms. - Pass
cartFetcherandcartRevalidate— together they let the block apply codes to the cart. - On multi-market stores, use a locale-aware
cartRoute, for example/fr-ca/cart.
For a complete working example, see app/components/cart/joy-redeem-cart.tsx in the public reference storefront (opens in a new tab).
2. Return the cart from your cart action
Your cart route's action must return the updated cart in its response. The Hydrogen skeleton's default CartForm action already returns {cart, errors, warnings} — keep that.
The block only trusts the cart its own request returns. Without it, the block can't confirm the discount and rolls back every apply.
3. Add the cart fragment fields
Merge these fields into your CART_QUERY_FRAGMENT. Pick the variant that matches your Storefront API version.
Default — Storefront API 2025-10 to 2026-10:
fragment JoyRedeemCartDefault on Cart {
discountCodes { code applicable }
cost { subtotalAmount { amount currencyCode } totalAmount { amount currencyCode } }
discountAllocations { targetType discountedAmount { amount currencyCode } ... on CartCodeDiscountAllocation { code } }
lines(first: $numCartLines) { nodes {
id quantity
cost { subtotalAmount { amount currencyCode } totalAmount { amount currencyCode } }
discountAllocations { targetType discountedAmount { amount currencyCode } ... on CartCodeDiscountAllocation { code } }
merchandise { ... on ProductVariant { id product { id collections(first: 20) { nodes { id } pageInfo { hasNextPage } } } } }
} }
}Modern — Storefront API 2026-07 and later only (it fails on 2026-04 and earlier):
fragment JoyRedeemCartModern on Cart {
discountCodes { code applicable }
cost { subtotalAmount { amount currencyCode } totalAmount { amount currencyCode } }
lines(first: $numCartLines) { nodes {
id quantity
cost { subtotalAmount { amount currencyCode } totalAmount { amount currencyCode } }
discountAllocations { targetType discountedAmount { amount currencyCode } ... on CartCodeDiscountAllocation { code } }
joyAllDiscounts: discountAllocations(lineLevelOnly: false) { targetType discountedAmount { amount currencyCode } ... on CartCodeDiscountAllocation { code } }
merchandise { ... on ProductVariant { id product { id collections(first: 20) { nodes { id } pageInfo { hasNextPage } } } } }
} }
deliveryGroups(first: 10) { nodes { discountAllocations { targetType discountedAmount { amount currencyCode } ... on CartCodeDiscountAllocation { code } } } }
}collections(first: 20)is optional. Without it — or when a product is in more than 20 collections — rewards limited to collections are hidden.- If the discount allocation fields are missing while a code is applied, the block disables redemption.
Props
| Prop | Required | What it does |
|---|---|---|
apiUrl | Yes | The Joy headless API URL (JOY_API_URL). |
shopDomain | Yes | Your myshopify domain. |
initialData | Yes | The root joy data — the same value <JoyWidget/> takes. |
cart | No | The storefront cart, with the fragment fields above. Falls back to <JoyProvider cart>. |
cartFetcher, cartRevalidate | No | Together they let the block apply codes to the cart. Without them, the block uses copy-code mode. |
cartRoute | No | Your cart action route. Defaults to /cart. |
onCartUpdated | No | Receives the updated cart after each change the block makes. |
tokenRoute | No | Defaults to /api/joy-token. |
loginUrl | No | Overrides the guest Sign in link. |
onNavigate | No | Client-side navigation, e.g. useNavigate(). Without it, links do a full page load. |
assetUrl | No | Overrides where Joy code loads from. Leave unset in production. |
className | No | A class for the block's wrapper. |
Customize the design
The block uses your Redeem in Cart settings in Joy admin — the same settings as the Online Store theme block. Change colors, texts and options there, and your Hydrogen cart picks them up. See Redeem in Cart.
Go to On-site content > Cart drawer > Redeem in cart to edit them.
Behavior settings
These options live under Behavior in the same settings page and apply to the headless block too:
| Setting | What it does on Hydrogen |
|---|---|
| Enable customer self-revoke coupon | Customers can remove a redeemed coupon from the cart drawer, and the points are refunded. When it's off, the remove/revoke action is hidden. |
| Automatically remove failed discount codes | If a redeemed code fails to apply to the cart, the block removes it and refunds its points, keeping the cart clean. |
| Show points input for dynamic rewards | Customers can type how many points to spend. When it's off, they choose with the slider only. |
| Replace applied reward when a new one saves more | Not supported on Hydrogen yet. A new reward that can't be combined with an applied one is rejected, as if this setting were off. |
Guests and signed-in customers
- Signed-in customers see their balance, the rewards they can redeem and the rewards already applied.
- Guests see how many points the order will earn and a Sign in link. If you turned on hiding the block for guests, they see nothing.
Copy-code mode
If you don't pass cartFetcher and cartRevalidate, customers can still redeem. The block shows the code with a Copy button and asks the customer to apply it at checkout. Codes are remembered for the current browser session only, and never shown to a guest or another customer.
Limitations
- Base currency only. When the cart's currency differs from your store's base currency, the block doesn't show. Carts in your store currency aren't affected.
- Earn-back is an estimate. On stores using rule-based earning, place-order rules limited to a collection, tag or vendor aren't matched in the cart, so the "Earn X points" line can be lower than on your theme. Points are awarded correctly on the order.
- Replace applied reward when a new one saves more isn't supported yet (planned).
- Empty cart. The block doesn't show when the cart is empty — same as the theme block.
Verify
- Add a product to the cart and open the drawer — the block shows.
- As a guest, you see the earn-back line and Sign in.
- Signed in with enough points, redeem a reward — the discount appears in the cart total.
- Remove the applied reward — the discount leaves the cart.
- Change a color in Joy admin Redeem in Cart settings — your cart reflects it.
Troubleshooting
| What you see | Cause | Fix |
|---|---|---|
| Every apply rolls back right away | Your cart action doesn't return cart, or the cart fragment is missing fields. | Return {cart, errors, warnings} from the cart action and merge the fragment fields above. |
| The block doesn't show | The cart is empty, the cart currency isn't your base currency, or the block is hidden for guests. | Add a product, test in your base currency, and check your Redeem in Cart settings. |
[joy] JoyRedeemCart: … warning in the console | The Joy widget code is outdated or failed to load. | Leave JOY_WIDGET_ASSET_URL unset in production and check your CSP. |
| Code shows with a Copy button instead of applying | cartFetcher or cartRevalidate isn't passed. | Pass both props. |
| Rewards for some collections are missing | collections isn't in the fragment, or a product is in more than 20 collections. | Add collections(first: 20) to the fragment. |
Signed-in customer sees the guest state, or /api/joy-token returns 401 | JOY_API_SECRET is wrong or revoked. | Copy the current Headless secret from Joy admin. See Troubleshooting. |