@forgecart/cli 2.202606151453.0 → 2.202607070306.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.
@@ -1,19 +1,40 @@
1
1
  'use client';
2
2
 
3
- import type { Product, ProductVariant } from '@forgecart/sdk/shop';
4
- import { useMemo, useState } from 'react';
3
+ import { useEffect, useMemo, useState } from 'react';
5
4
 
6
5
  import { useCart } from '../lib/cart-context';
7
- import { formatPrice } from '../lib/forgecart';
6
+ import {
7
+ formatPrice,
8
+ getPlanCadenceLabel,
9
+ getPlanPreviewPrice,
10
+ getPlanSavingsLabel,
11
+ type Product,
12
+ type ProductVariant,
13
+ type SellingPlan,
14
+ type SellingPlanGroup,
15
+ } from '../lib/forgecart';
8
16
 
9
17
  /**
10
- * Variant picker + add-to-cart for the product detail page.
18
+ * Variant picker + purchase-options + add-to-cart for the product detail page.
11
19
  *
12
20
  * The customer picks one value per option group (Size, Color, …); the matching
13
21
  * variant drives the displayed price and what gets added to the cart — you add a
14
22
  * **variant**, never the product. Single-variant products skip the selectors.
23
+ *
24
+ * When the selected variant has subscription selling plans (passed in as
25
+ * `planGroupsByVariant`, fetched server-side by the page), a "Purchase options"
26
+ * control appears above Add-to-cart: a one-time purchase plus one option per
27
+ * plan, each with a price preview and a savings/trial hint. The chosen plan id
28
+ * (or `undefined` for one-time) flows into the cart on Add. Variants with no
29
+ * plans keep the plain one-time add-to-cart path unchanged.
15
30
  */
16
- export function ProductPurchase({ product }: { product: Product }) {
31
+ export function ProductPurchase({
32
+ product,
33
+ planGroupsByVariant = {},
34
+ }: {
35
+ product: Product;
36
+ planGroupsByVariant?: Record<string, SellingPlanGroup[]>;
37
+ }) {
17
38
  const { add, pending } = useCart();
18
39
  const [added, setAdded] = useState(false);
19
40
 
@@ -36,19 +57,77 @@ export function ProductPurchase({ product }: { product: Product }) {
36
57
  );
37
58
  }, [product.variants, selected]);
38
59
 
60
+ // The selling-plan groups the *selected* variant is eligible for.
61
+ const groups: SellingPlanGroup[] = useMemo(
62
+ () => (variant ? (planGroupsByVariant[variant.id] ?? []) : []),
63
+ [planGroupsByVariant, variant],
64
+ );
65
+
66
+ // The selectable plans (enabled only), flattened across applicable groups.
67
+ const plans: SellingPlan[] = useMemo(
68
+ () => groups.flatMap((g) => g.plans).filter((p) => p.enabled),
69
+ [groups],
70
+ );
71
+
72
+ // One-time is offered unless EVERY applicable group forbids it (subscription-only).
73
+ const oneTimeAllowed = groups.length === 0 || groups.some((g) => g.oneTimePurchaseAllowed);
74
+ const hasPurchaseOptions = plans.length > 0;
75
+
76
+ // `null` = one-time purchase; otherwise the chosen plan id.
77
+ const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
78
+
79
+ // The set of valid choices changes with the variant (and thus its plans). Re-
80
+ // normalize the selection whenever that signature changes: keep a still-valid
81
+ // choice, otherwise default to one-time when allowed, else the first plan —
82
+ // never leave a stale id that is no longer offered for the current variant.
83
+ // `planIds` is a stable, primitive signature of the current plan set, so the
84
+ // effect re-runs exactly when the options (or the one-time gate) change and
85
+ // closes over no array identities.
86
+ const planIds = plans.map((p) => p.id).join(',');
87
+ useEffect(() => {
88
+ const ids = planIds ? planIds.split(',') : [];
89
+ const validIds = new Set(ids);
90
+ setSelectedPlanId((current) => {
91
+ if (current !== null && validIds.has(current)) {
92
+ return current;
93
+ }
94
+ if (oneTimeAllowed) {
95
+ return null;
96
+ }
97
+ return ids[0] ?? null;
98
+ });
99
+ }, [planIds, oneTimeAllowed]);
100
+
101
+ const selectedPlan: SellingPlan | undefined = useMemo(
102
+ () => plans.find((p) => p.id === selectedPlanId),
103
+ [plans, selectedPlanId],
104
+ );
105
+
106
+ // Price preview for the chosen option: one-time shows the variant price; a
107
+ // plan shows its policy-adjusted per-unit price.
108
+ const previewPrice = useMemo(() => {
109
+ if (!variant) {
110
+ return null;
111
+ }
112
+ if (!selectedPlan) {
113
+ return variant.priceWithTax;
114
+ }
115
+ return getPlanPreviewPrice(variant.priceWithTax, selectedPlan);
116
+ }, [variant, selectedPlan]);
117
+
39
118
  const onAdd = () => {
40
119
  if (!variant) {
41
120
  return;
42
121
  }
43
- add(variant.id, 1);
122
+ add(variant.id, 1, selectedPlanId ?? undefined);
44
123
  setAdded(true);
45
124
  window.setTimeout(() => setAdded(false), 1500);
46
125
  };
47
126
 
48
127
  return (
49
128
  <div className="space-y-4">
50
- {variant ? (
51
- <p className="text-xl font-semibold text-gray-900">{formatPrice(variant.priceWithTax)}</p>
129
+ {variant && previewPrice !== null ? (
130
+ <p className="text-xl font-semibold text-gray-900">{formatPrice(previewPrice)}</p>
52
131
  ) : (
53
132
  <p className="text-sm text-gray-500">Select options to see the price.</p>
54
133
  )}
@@ -78,6 +157,38 @@ export function ProductPurchase({ product }: { product: Product }) {
78
157
  </div>
79
158
  ))}
80
159
 
160
+ {variant && hasPurchaseOptions && (
161
+ <div>
162
+ <h3 className="mb-1 text-sm font-medium text-gray-700">Purchase options</h3>
163
+ <div className="flex flex-col gap-2">
164
+ {oneTimeAllowed && (
165
+ <PurchaseOption
166
+ label="One-time purchase"
167
+ priceLabel={formatPrice(variant.priceWithTax)}
168
+ active={selectedPlanId === null}
169
+ onSelect={() => setSelectedPlanId(null)}
170
+ />
171
+ )}
172
+ {plans.map((plan) => {
173
+ const savings = getPlanSavingsLabel(plan);
174
+ const hint = [savings, plan.trialDays > 0 ? `${plan.trialDays}-day free trial` : null]
175
+ .filter(Boolean)
176
+ .join(' · ');
177
+ return (
178
+ <PurchaseOption
179
+ key={plan.id}
180
+ label={`Subscribe — ${getPlanCadenceLabel(plan)}`}
181
+ hint={hint || undefined}
182
+ priceLabel={formatPrice(getPlanPreviewPrice(variant.priceWithTax, plan))}
183
+ active={selectedPlanId === plan.id}
184
+ onSelect={() => setSelectedPlanId(plan.id)}
185
+ />
186
+ );
187
+ })}
188
+ </div>
189
+ </div>
190
+ )}
191
+
81
192
  <button
82
193
  type="button"
83
194
  onClick={onAdd}
@@ -89,3 +200,37 @@ export function ProductPurchase({ product }: { product: Product }) {
89
200
  </div>
90
201
  );
91
202
  }
203
+
204
+ /** A single selectable purchase option (one-time or a subscription plan). */
205
+ function PurchaseOption({
206
+ label,
207
+ hint,
208
+ priceLabel,
209
+ active,
210
+ onSelect,
211
+ }: {
212
+ label: string;
213
+ hint?: string;
214
+ priceLabel: string;
215
+ active: boolean;
216
+ onSelect: () => void;
217
+ }) {
218
+ return (
219
+ <button
220
+ type="button"
221
+ onClick={onSelect}
222
+ aria-pressed={active}
223
+ className={`flex items-center justify-between gap-3 rounded-md border px-3 py-2 text-left text-sm transition ${
224
+ active
225
+ ? 'border-gray-900 ring-1 ring-gray-900'
226
+ : 'border-gray-300 hover:border-gray-900'
227
+ }`}
228
+ >
229
+ <span className="min-w-0">
230
+ <span className="block font-medium text-gray-900">{label}</span>
231
+ {hint && <span className="block text-xs text-gray-500">{hint}</span>}
232
+ </span>
233
+ <span className="shrink-0 font-medium text-gray-900">{priceLabel}</span>
234
+ </button>
235
+ );
236
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * DEV-ONLY server-error beacon via Next's `onRequestError` instrumentation hook.
3
+ *
4
+ * This is the PRIMARY server-side crash-detection surface for the in-pod dev-server:
5
+ * when a Server Component, a server action, or a route handler THROWS while rendering
6
+ * a request under `next dev`, Next invokes `onRequestError` with the structured error
7
+ * + request + routing context. The hook POSTs that detail SERVER-side, straight to the
8
+ * in-pod workspace-manager loopback receiver at
9
+ * `http://127.0.0.1:<FORGE_BEACON_PORT>/__forge_beacon`, which folds it into the
10
+ * supervisor's runtime state so the shop's recovery brain can originate a fix for a
11
+ * crash the `/ping` liveness probe cannot see (the dev-server process stays up and
12
+ * `/ping` stays 200, but `/` 500s per request).
13
+ *
14
+ * This complements the browser beacon (`ForgeErrorBeacon`): that one catches
15
+ * CLIENT-component / hydration throws that surface only in the browser; this one
16
+ * catches SERVER render throws that surface only in the dev-server. Both POST the
17
+ * same `/__forge_beacon` shape — the only difference is `source` (`'browser'` vs
18
+ * `'server'`).
19
+ *
20
+ * Gated entirely on `NODE_ENV === 'development'`: a deployed `next start` storefront
21
+ * registers no beacon (the literal check is inlined by the bundler, so the body is
22
+ * dead-code-eliminated from a production build) and the loopback receiver does not
23
+ * exist outside the pod. Next compiles `instrumentation.ts` for BOTH the Node and Edge
24
+ * runtimes; the loopback POST is a Node-only concern, so the hook also gates on the
25
+ * Node runtime — the Edge bundle then carries an inert no-op.
26
+ *
27
+ * The hook NEVER throws out of itself: a failed delivery is swallowed (the beacon is a
28
+ * best-effort backstop, never a hard dependency of the request), so a flapping receiver
29
+ * can never mask or replace the underlying request error Next is already reporting.
30
+ */
31
+
32
+ const BEACON_PORT = process.env.FORGE_BEACON_PORT ?? '3002';
33
+
34
+ interface OnRequestErrorRequest {
35
+ path: string;
36
+ method: string;
37
+ headers: { [key: string]: string | string[] | undefined };
38
+ }
39
+
40
+ interface OnRequestErrorContext {
41
+ routerKind: string;
42
+ routePath: string;
43
+ routeType: string;
44
+ }
45
+
46
+ export async function onRequestError(
47
+ error: unknown,
48
+ request: OnRequestErrorRequest,
49
+ context: OnRequestErrorContext,
50
+ ): Promise<void> {
51
+ if (process.env.NODE_ENV !== 'development' || process.env.NEXT_RUNTIME !== 'nodejs') {
52
+ return;
53
+ }
54
+
55
+ const err = error as { message?: string; digest?: string } | undefined;
56
+ const body = JSON.stringify({
57
+ message: typeof err?.message === 'string' ? err.message : String(error),
58
+ digest: typeof err?.digest === 'string' ? err.digest : undefined,
59
+ route: request.path,
60
+ routeType: context.routeType,
61
+ source: 'server',
62
+ });
63
+
64
+ await fetch(`http://127.0.0.1:${BEACON_PORT}/__forge_beacon`, {
65
+ method: 'POST',
66
+ headers: { 'content-type': 'application/json' },
67
+ body,
68
+ }).catch(() => undefined);
69
+ }
@@ -1,148 +1,131 @@
1
1
  'use server';
2
2
 
3
- import type { Order } from '@forgecart/sdk/shop';
3
+ import { ForgeCartShopClient } from '@forgecart/sdk';
4
+ import type { ShopAddItemToOrderInput } from '@forgecart/sdk/shop';
4
5
  import { cookies } from 'next/headers';
5
6
 
7
+ import type { Order } from './forgecart';
8
+
6
9
  /**
7
10
  * Server-side cart, backed by the ForgeCart shop **order** API (the real cart),
8
- * not browser storage. Every export here is a Server Action returning the live
9
- * `Order` (the SDK type) so callers render straight off it.
11
+ * not browser storage. Every export here is a Server Action driving the SDK's
12
+ * generated, typed cart operations and returning the live `Order` (the SDK
13
+ * type) so callers render straight off it.
10
14
  *
11
- * The anonymous active order is tracked by a session token: the shop API returns
12
- * a fresh token in the `forgecart-auth-token` response header on the first
13
- * mutation, which we persist into the httpOnly `forgecart-session` cookie and
14
- * send back as a bearer token on every subsequent request. The channel token and
15
- * the session never reach the browser all cart calls run here, on the server.
15
+ * The anonymous active order is tracked by a session token: the shop API
16
+ * returns a fresh token (result extensions) on the first mutation, which the
17
+ * SDK client captures — we persist it into the httpOnly `forgecart-session`
18
+ * cookie and replay it as the client's auth token on every subsequent request.
19
+ * Session state is per shopper, so each action builds a short-lived client
20
+ * around its own request's cookie (queries/mutations ride plain fetch — the
21
+ * client only opens a websocket for subscriptions, which the cart never uses).
22
+ * The channel token and the session never reach the browser — all cart calls
23
+ * run here, on the server.
16
24
  */
17
25
 
18
26
  const SHOP_API_URL = process.env.FORGECART_SHOP_API_URL ?? '';
19
27
  const CHANNEL_TOKEN = process.env.FORGECART_CHANNEL_TOKEN ?? '';
20
28
  const SESSION_COOKIE = 'forgecart-session';
21
29
 
22
- /** The order fields the storefront renders (a subset of the SDK `Order` type). */
23
- const ORDER_FIELDS = `
24
- id
25
- totalQuantity
26
- subTotalWithTax
27
- totalWithTax
28
- currencyCode
29
- lines {
30
- id
31
- quantity
32
- unitPriceWithTax
33
- linePriceWithTax
34
- productVariant {
35
- id
36
- name
37
- }
38
- featuredAsset {
39
- preview
40
- }
30
+ /** A per-request shop client carrying this shopper's session, when one exists. */
31
+ async function getCartClient(): Promise<ForgeCartShopClient> {
32
+ const store = await cookies();
33
+ const session = store.get(SESSION_COOKIE)?.value;
34
+ const client = new ForgeCartShopClient({
35
+ endpoint: SHOP_API_URL,
36
+ channelToken: CHANNEL_TOKEN,
37
+ });
38
+ if (session) {
39
+ client.setAuthToken(session);
41
40
  }
42
- `;
43
-
44
- interface GraphQLResponse<T> {
45
- data?: T;
46
- errors?: { message: string }[];
41
+ return client;
47
42
  }
48
43
 
49
44
  /**
50
- * Call the channel's shop API for a cart operation.
45
+ * Persist the session token the client captured during a mutation.
51
46
  *
52
- * `persist` MUST be `false` when called from a Server Component (the initial
53
- * cart read): Server Components cannot set cookies only Server Actions (the
54
- * mutations) can, so they capture/refresh the session token.
47
+ * MUST only be called from Server Actions (the mutations): Server Components
48
+ * cannot set cookies, which is why the read path never persists.
55
49
  */
56
- async function shopFetch<T>(
57
- query: string,
58
- variables: Record<string, unknown>,
59
- persist: boolean,
60
- ): Promise<GraphQLResponse<T>> {
50
+ async function persistSession(client: ForgeCartShopClient): Promise<void> {
51
+ const token = client.getAuthToken();
52
+ if (!token) return;
61
53
  const store = await cookies();
62
- const session = store.get(SESSION_COOKIE)?.value;
63
-
64
- const headers: Record<string, string> = {
65
- 'content-type': 'application/json',
66
- 'forgecart-token': CHANNEL_TOKEN,
67
- };
68
- if (session) {
69
- headers.authorization = `Bearer ${session}`;
70
- }
71
-
72
- const res = await fetch(SHOP_API_URL, {
73
- method: 'POST',
74
- headers,
75
- body: JSON.stringify({ query, variables }),
76
- cache: 'no-store',
54
+ if (store.get(SESSION_COOKIE)?.value === token) return;
55
+ store.set(SESSION_COOKIE, token, {
56
+ httpOnly: true,
57
+ sameSite: 'lax',
58
+ path: '/',
59
+ maxAge: 60 * 60 * 24 * 30,
77
60
  });
78
-
79
- if (persist) {
80
- const token = res.headers.get('forgecart-auth-token');
81
- if (token && token !== session) {
82
- store.set(SESSION_COOKIE, token, {
83
- httpOnly: true,
84
- sameSite: 'lax',
85
- path: '/',
86
- maxAge: 60 * 60 * 24 * 30,
87
- });
88
- }
89
- }
90
-
91
- return (await res.json()) as GraphQLResponse<T>;
92
61
  }
93
62
 
94
- function unwrap<T>(res: GraphQLResponse<T>): T {
95
- if (res.errors?.length) {
96
- throw new Error(res.errors[0].message);
97
- }
98
- if (!res.data) {
99
- throw new Error('Empty response from shop API');
100
- }
101
- return res.data;
63
+ /**
64
+ * Read the active order (cart). Safe to call from a Server Component.
65
+ *
66
+ * Before `forgecart init` writes `.env.local` (notably the image-build pre-warm,
67
+ * which renders `/` with no env), there is no shop to talk to — return an empty
68
+ * cart immediately instead of driving the client into a doomed fetch. The
69
+ * layout renders on every route, so this read must never stall an unconfigured
70
+ * scaffold.
71
+ */
72
+ export async function getCart(): Promise<Order | null> {
73
+ if (!SHOP_API_URL || !CHANNEL_TOKEN) return null;
74
+ const client = await getCartClient();
75
+ const { activeOrder } = await client.order.activeOrder();
76
+ return activeOrder ?? null;
102
77
  }
103
78
 
104
- /** Read the active order (cart). Safe to call from a Server Component. */
105
- export async function getCart(): Promise<Order | null> {
106
- const res = await shopFetch<{ activeOrder: Order | null }>(
107
- `query { activeOrder { ${ORDER_FIELDS} } }`,
108
- {},
109
- false,
110
- );
111
- return res.data?.activeOrder ?? null;
79
+ /**
80
+ * Add a variant to the cart (creates the order + session on first call).
81
+ *
82
+ * Pass `sellingPlanId` to add the line as a subscription on the given plan;
83
+ * omit it (the default) for a one-time purchase. The id is only included in the
84
+ * mutation input when present, so the one-time path sends the same shape as
85
+ * before.
86
+ */
87
+ export async function addToCart(
88
+ variantId: string,
89
+ quantity = 1,
90
+ sellingPlanId?: string,
91
+ ): Promise<Order | null> {
92
+ const input: ShopAddItemToOrderInput = { variantId, quantity };
93
+ if (sellingPlanId) {
94
+ input.sellingPlanId = sellingPlanId;
95
+ }
96
+ const client = await getCartClient();
97
+ const { addItemToOrder } = await client.cart.addItemToOrder({ input });
98
+ await persistSession(client);
99
+ return addItemToOrder;
112
100
  }
113
101
 
114
- /** Add a variant to the cart (creates the order + session on first call). */
115
- export async function addToCart(variantId: string, quantity = 1): Promise<Order | null> {
116
- const data = unwrap(
117
- await shopFetch<{ addItemToOrder: Order }>(
118
- `mutation($input: ShopAddItemToOrderInput!) { addItemToOrder(input: $input) { ${ORDER_FIELDS} } }`,
119
- { input: { variantId, quantity } },
120
- true,
121
- ),
122
- );
123
- return data.addItemToOrder;
102
+ /**
103
+ * Set (or clear) the subscription plan for the whole order.
104
+ *
105
+ * Pass a channel-wide selling plan id to subscribe the entire cart on that
106
+ * plan, or `null` to clear it back to a one-time order. Returns the live order.
107
+ */
108
+ export async function setCartSellingPlan(sellingPlanId: string | null): Promise<Order | null> {
109
+ const client = await getCartClient();
110
+ const { setOrderSellingPlan } = await client.cart.setOrderSellingPlan({
111
+ input: { sellingPlanId },
112
+ });
113
+ await persistSession(client);
114
+ return setOrderSellingPlan;
124
115
  }
125
116
 
126
117
  /** Set the quantity of a cart line (0 removes it). */
127
118
  export async function updateLine(lineId: string, quantity: number): Promise<Order | null> {
128
- const data = unwrap(
129
- await shopFetch<{ adjustOrderLine: Order }>(
130
- `mutation($input: ShopAdjustOrderLineInput!) { adjustOrderLine(input: $input) { ${ORDER_FIELDS} } }`,
131
- { input: { lineId, quantity } },
132
- true,
133
- ),
134
- );
135
- return data.adjustOrderLine;
119
+ const client = await getCartClient();
120
+ const { adjustOrderLine } = await client.cart.adjustOrderLine({ input: { lineId, quantity } });
121
+ await persistSession(client);
122
+ return adjustOrderLine;
136
123
  }
137
124
 
138
125
  /** Remove a line from the cart. */
139
126
  export async function removeLine(lineId: string): Promise<Order | null> {
140
- const data = unwrap(
141
- await shopFetch<{ removeOrderLine: Order }>(
142
- `mutation($lineId: ID!) { removeOrderLine(lineId: $lineId) { ${ORDER_FIELDS} } }`,
143
- { lineId },
144
- true,
145
- ),
146
- );
147
- return data.removeOrderLine;
127
+ const client = await getCartClient();
128
+ const { removeOrderLine } = await client.cart.removeOrderLine({ lineId });
129
+ await persistSession(client);
130
+ return removeOrderLine;
148
131
  }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import type { Order } from '@forgecart/sdk/shop';
3
+ import type { Order } from './forgecart';
4
4
  import {
5
5
  createContext,
6
6
  useCallback,
@@ -11,7 +11,13 @@ import {
11
11
  type ReactNode,
12
12
  } from 'react';
13
13
 
14
- import { addToCart, getCart, removeLine as removeLineAction, updateLine } from './cart-actions';
14
+ import {
15
+ addToCart,
16
+ getCart,
17
+ removeLine as removeLineAction,
18
+ setCartSellingPlan,
19
+ updateLine,
20
+ } from './cart-actions';
15
21
 
16
22
  /**
17
23
  * Cart state backed by the ForgeCart shop order API (see `cart-actions.ts`).
@@ -26,9 +32,10 @@ interface CartContextValue {
26
32
  itemCount: number;
27
33
  subtotal: number;
28
34
  pending: boolean;
29
- add: (variantId: string, quantity?: number) => void;
35
+ add: (variantId: string, quantity?: number, sellingPlanId?: string) => void;
30
36
  setQuantity: (lineId: string, quantity: number) => void;
31
37
  remove: (lineId: string) => void;
38
+ setSellingPlan: (sellingPlanId: string | null) => void;
32
39
  refresh: () => void;
33
40
  }
34
41
 
@@ -51,7 +58,8 @@ export function CartProvider({
51
58
  }, []);
52
59
 
53
60
  const add = useCallback(
54
- (variantId: string, quantity = 1) => run(() => addToCart(variantId, quantity)),
61
+ (variantId: string, quantity = 1, sellingPlanId?: string) =>
62
+ run(() => addToCart(variantId, quantity, sellingPlanId)),
55
63
  [run],
56
64
  );
57
65
  const setQuantity = useCallback(
@@ -59,6 +67,10 @@ export function CartProvider({
59
67
  [run],
60
68
  );
61
69
  const remove = useCallback((lineId: string) => run(() => removeLineAction(lineId)), [run]);
70
+ const setSellingPlan = useCallback(
71
+ (sellingPlanId: string | null) => run(() => setCartSellingPlan(sellingPlanId)),
72
+ [run],
73
+ );
62
74
  const refresh = useCallback(() => run(() => getCart()), [run]);
63
75
 
64
76
  const value = useMemo<CartContextValue>(
@@ -70,9 +82,10 @@ export function CartProvider({
70
82
  add,
71
83
  setQuantity,
72
84
  remove,
85
+ setSellingPlan,
73
86
  refresh,
74
87
  }),
75
- [cart, pending, add, setQuantity, remove, refresh],
88
+ [cart, pending, add, setQuantity, remove, setSellingPlan, refresh],
76
89
  );
77
90
 
78
91
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;