@burdenoff/microfe-billing 2026.1027.2 → 2026.1028.1

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 +1,2 @@
1
1
  import "./useAddons.js";
2
+ import "./useAddonPurchaseEligibility.js";
@@ -0,0 +1,27 @@
1
+ import { useGetAddonPurchaseEligibilityQuery as e } from "../../../../generated/global-operations.js";
2
+ import { useMemo as t } from "react";
3
+ //#region src/billing/modules/addons/hooks/useAddonPurchaseEligibility.ts
4
+ function n(n) {
5
+ let r = t(() => [...new Set(n)].sort(), [n]), { data: i, loading: a } = e({
6
+ variables: { addonIds: r },
7
+ skip: r.length === 0,
8
+ fetchPolicy: "cache-and-network"
9
+ }), o = t(() => {
10
+ let e = /* @__PURE__ */ new Map();
11
+ for (let t of i?.addonPurchaseEligibility ?? []) !t.eligible && t.reason !== "NOT_FOUND" && e.set(t.addonId, {
12
+ addonId: t.addonId,
13
+ productId: t.productId ?? null,
14
+ reason: t.reason
15
+ });
16
+ return e;
17
+ }, [i]);
18
+ return {
19
+ blocked: o,
20
+ isBlocked: (e) => o.has(e),
21
+ isLoading: a
22
+ };
23
+ }
24
+ //#endregion
25
+ export { n as useAddonPurchaseEligibility };
26
+
27
+ //# sourceMappingURL=useAddonPurchaseEligibility.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAddonPurchaseEligibility.js","names":[],"sources":["../../../../../src/billing/modules/addons/hooks/useAddonPurchaseEligibility.ts"],"sourcesContent":["/**\n * Addons Module - useAddonPurchaseEligibility Hook\n *\n * BOFF-9889: add-ons require a paid subscription to their product (the\n * catalogs' \"Free tier exclusion\" rule). The backend enforces it at checkout;\n * this hook asks the same rule up front so the browse and detail pages can\n * explain the restriction instead of letting a Free workspace fill a cart it\n * cannot check out.\n */\n\nimport { useMemo } from 'react';\nimport { useGetAddonPurchaseEligibilityQuery } from '../../../../generated/global-operations';\n\nexport interface AddonPurchaseBlock {\n addonId: string;\n productId: string | null;\n reason: string;\n}\n\ninterface UseAddonPurchaseEligibilityResult {\n /** Add-ons the organization cannot buy yet, keyed by add-on id. */\n blocked: Map<string, AddonPurchaseBlock>;\n isBlocked: (addonId: string) => boolean;\n isLoading: boolean;\n}\n\nexport function useAddonPurchaseEligibility(addonIds: string[]): UseAddonPurchaseEligibilityResult {\n const ids = useMemo(() => [...new Set(addonIds)].sort(), [addonIds]);\n const { data, loading } = useGetAddonPurchaseEligibilityQuery({\n variables: { addonIds: ids },\n skip: ids.length === 0,\n fetchPolicy: 'cache-and-network',\n });\n\n const blocked = useMemo(() => {\n const map = new Map<string, AddonPurchaseBlock>();\n for (const result of data?.addonPurchaseEligibility ?? []) {\n if (!result.eligible && result.reason !== 'NOT_FOUND') {\n map.set(result.addonId, {\n addonId: result.addonId,\n productId: result.productId ?? null,\n reason: result.reason,\n });\n }\n }\n return map;\n }, [data]);\n\n return {\n blocked,\n isBlocked: (addonId: string) => blocked.has(addonId),\n isLoading: loading,\n };\n}\n"],"mappings":";;;AA0BA,SAAgB,EAA4B,GAAuD;CACjG,IAAM,IAAM,QAAc,CAAC,GAAG,IAAI,IAAI,CAAQ,CAAC,EAAE,KAAK,GAAG,CAAC,CAAQ,CAAC,GAC7D,EAAE,SAAM,eAAY,EAAoC;EAC5D,WAAW,EAAE,UAAU,EAAI;EAC3B,MAAM,EAAI,WAAW;EACrB,aAAa;CACf,CAAC,GAEK,IAAU,QAAc;EAC5B,IAAM,oBAAM,IAAI,IAAgC;EAChD,KAAK,IAAM,KAAU,GAAM,4BAA4B,CAAC,GACtD,AAAI,CAAC,EAAO,YAAY,EAAO,WAAW,eACxC,EAAI,IAAI,EAAO,SAAS;GACtB,SAAS,EAAO;GAChB,WAAW,EAAO,aAAa;GAC/B,QAAQ,EAAO;EACjB,CAAC;EAGL,OAAO;CACT,GAAG,CAAC,CAAI,CAAC;CAET,OAAO;EACL;EACA,YAAY,MAAoB,EAAQ,IAAI,CAAO;EACnD,WAAW;CACb;AACF"}
@@ -1,4 +1,5 @@
1
1
  import "./hooks/useAddons.js";
2
+ import "./hooks/useAddonPurchaseEligibility.js";
2
3
  import "./hooks/index.js";
3
4
  import { AddonsListPage as e } from "./pages/AddonsListPage.js";
4
5
  import { AddonDetailPage as t } from "./pages/AddonDetailPage.js";
@@ -7,15 +7,16 @@ import "../../../shared/utils/index.js";
7
7
  import { useAddonCartStore as o } from "../store/addonCartStore.js";
8
8
  import { useBillingNavigate as s } from "../../../hooks/useBillingNavigate.js";
9
9
  import { useAddon as c } from "../hooks/useAddons.js";
10
+ import { useAddonPurchaseEligibility as l } from "../hooks/useAddonPurchaseEligibility.js";
10
11
  import "../hooks/index.js";
11
- import { useMemo as l } from "react";
12
- import { Minus as u, Plus as d, ShoppingCart as f, Trash2 as p } from "lucide-react";
13
- import { useParams as m } from "react-router";
14
- import { jsx as h, jsxs as g } from "react/jsx-runtime";
15
- import { useI18n as _ } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
16
- import { EmphasisPanel as v, PagePurpose as y } from "@burdenoff/fe-libs/ui";
12
+ import { useMemo as u } from "react";
13
+ import { Lock as d, Minus as f, Plus as p, ShoppingCart as m, Trash2 as h } from "lucide-react";
14
+ import { useParams as g } from "react-router";
15
+ import { jsx as _, jsxs as v } from "react/jsx-runtime";
16
+ import { useI18n as y } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
17
+ import { EmphasisPanel as b, PagePurpose as x } from "@burdenoff/fe-libs/ui";
17
18
  //#region src/billing/modules/addons/pages/AddonBrowseDetailPage.tsx
18
- function b(e) {
19
+ function S(e) {
19
20
  let t = /* @__PURE__ */ new Map();
20
21
  for (let n of e) {
21
22
  if (!n.quota) continue;
@@ -29,123 +30,123 @@ function b(e) {
29
30
  }
30
31
  return Array.from(t.values());
31
32
  }
32
- var x = () => {
33
- let { t: x } = _(), S = (e, t) => {
34
- let n = x(e);
33
+ var C = () => {
34
+ let { t: C } = y(), w = (e, t) => {
35
+ let n = C(e);
35
36
  return n === e ? t : n;
36
- }, { addonId: C } = m(), w = s(), { addon: T, isLoading: E, error: D, refetch: O } = c(C), { addToCart: k, removeFromCart: A, updateQuantity: j, isInCart: M, getCartItem: N } = o(), P = l(() => b(T?.features || []), [T?.features]), F = T ? M(T.id) : !1, I = (T ? N(T.id) : void 0)?.quantity || 0;
37
- return E ? /* @__PURE__ */ g("div", {
37
+ }, { addonId: T } = g(), E = s(), { addon: D, isLoading: O, error: k, refetch: A } = c(T), { addToCart: j, removeFromCart: M, updateQuantity: N, isInCart: P, getCartItem: F } = o(), I = u(() => S(D?.features || []), [D?.features]), L = D ? P(D.id) : !1, R = (D ? F(D.id) : void 0)?.quantity || 0, { isBlocked: z } = l(u(() => D ? [D.id] : [], [D])), B = D ? z(D.id) : !1;
38
+ return O ? /* @__PURE__ */ v("div", {
38
39
  className: "space-y-6 p-6",
39
- children: [/* @__PURE__ */ h("div", { className: "h-8 w-48 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ g("div", {
40
+ children: [/* @__PURE__ */ _("div", { className: "h-8 w-48 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ v("div", {
40
41
  className: "grid grid-cols-1 lg:grid-cols-3 gap-6",
41
- children: [/* @__PURE__ */ h("div", {
42
+ children: [/* @__PURE__ */ _("div", {
42
43
  className: "lg:col-span-2 space-y-4",
43
44
  children: [
44
45
  1,
45
46
  2,
46
47
  3
47
- ].map((e) => /* @__PURE__ */ h("div", { className: "h-24 bg-bg-sunken animate-pulse rounded" }, e))
48
- }), /* @__PURE__ */ h("div", { className: "h-64 bg-bg-sunken animate-pulse rounded" })]
48
+ ].map((e) => /* @__PURE__ */ _("div", { className: "h-24 bg-bg-sunken animate-pulse rounded" }, e))
49
+ }), /* @__PURE__ */ _("div", { className: "h-64 bg-bg-sunken animate-pulse rounded" })]
49
50
  })]
50
- }) : D && a(D) ? /* @__PURE__ */ h("div", {
51
+ }) : k && a(k) ? /* @__PURE__ */ _("div", {
51
52
  className: "p-6",
52
- children: /* @__PURE__ */ h(t, {
53
- title: S("billing.addons.serverUnavailable", "Server Unavailable"),
54
- message: S("billing.addons.unableToLoadAddonDetails", "Unable to load addon details."),
55
- onRetry: () => O(),
53
+ children: /* @__PURE__ */ _(t, {
54
+ title: w("billing.addons.serverUnavailable", "Server Unavailable"),
55
+ message: w("billing.addons.unableToLoadAddonDetails", "Unable to load addon details."),
56
+ onRetry: () => A(),
56
57
  showRetry: !0
57
58
  })
58
- }) : T ? /* @__PURE__ */ g("div", {
59
+ }) : D ? /* @__PURE__ */ v("div", {
59
60
  className: "space-y-6 p-6",
60
61
  children: [
61
- /* @__PURE__ */ h(e, {
62
- title: T.name,
63
- description: `${i(T.duration)} addon`
62
+ /* @__PURE__ */ _(e, {
63
+ title: D.name,
64
+ description: `${i(D.duration)} addon`
64
65
  }),
65
- /* @__PURE__ */ h(y, { children: S("billing.addons.browseDetailPurpose", "See exactly what this add-on costs and the extra capacity it gives you on top of your plan before you buy it. Add it to your cart from here when it fits your needs.") }),
66
- /* @__PURE__ */ g("div", {
66
+ /* @__PURE__ */ _(x, { children: w("billing.addons.browseDetailPurpose", "See exactly what this add-on costs and the extra capacity it gives you on top of your plan before you buy it. Add it to your cart from here when it fits your needs.") }),
67
+ /* @__PURE__ */ v("div", {
67
68
  className: "grid grid-cols-1 lg:grid-cols-3 gap-6",
68
- children: [/* @__PURE__ */ g("div", {
69
+ children: [/* @__PURE__ */ v("div", {
69
70
  className: "lg:col-span-2 space-y-6",
70
- children: [/* @__PURE__ */ g(v, {
71
+ children: [/* @__PURE__ */ v(b, {
71
72
  className: "overflow-hidden p-0",
72
- children: [/* @__PURE__ */ h("div", {
73
+ children: [/* @__PURE__ */ _("div", {
73
74
  className: "p-4 border-b border-border-subtle",
74
- children: /* @__PURE__ */ h("h2", {
75
+ children: /* @__PURE__ */ _("h2", {
75
76
  className: "font-semibold text-text-primary",
76
- children: S("billing.addons.pricing", "Pricing")
77
+ children: w("billing.addons.pricing", "Pricing")
77
78
  })
78
- }), /* @__PURE__ */ g("div", {
79
+ }), /* @__PURE__ */ v("div", {
79
80
  className: "p-6",
80
- children: [/* @__PURE__ */ g("div", {
81
+ children: [/* @__PURE__ */ v("div", {
81
82
  className: "flex items-baseline gap-2",
82
- children: [/* @__PURE__ */ h("span", {
83
+ children: [/* @__PURE__ */ _("span", {
83
84
  className: "text-4xl font-bold text-text-primary",
84
- children: n(T.price, T.currency)
85
- }), /* @__PURE__ */ g("span", {
85
+ children: n(D.price, D.currency)
86
+ }), /* @__PURE__ */ v("span", {
86
87
  className: "text-lg text-text-secondary",
87
- children: ["/ ", r(T.duration)]
88
+ children: ["/ ", r(D.duration)]
88
89
  })]
89
- }), /* @__PURE__ */ g("p", {
90
+ }), /* @__PURE__ */ v("p", {
90
91
  className: "text-sm text-text-secondary mt-2",
91
92
  children: [
92
- S("billing.addons.billed", "Billed"),
93
+ w("billing.addons.billed", "Billed"),
93
94
  " ",
94
- T.duration === "monthly" ? S("billing.addons.everyMonth", "every month") : S("billing.addons.annually", "annually")
95
+ D.duration === "monthly" ? w("billing.addons.everyMonth", "every month") : w("billing.addons.annually", "annually")
95
96
  ]
96
97
  })]
97
98
  })]
98
- }), /* @__PURE__ */ g("div", {
99
+ }), /* @__PURE__ */ v("div", {
99
100
  className: "border border-border-subtle rounded-lg bg-bg-surface overflow-hidden",
100
- children: [/* @__PURE__ */ h("div", {
101
+ children: [/* @__PURE__ */ _("div", {
101
102
  className: "p-4 border-b border-border-subtle bg-bg-sunken/30",
102
- children: /* @__PURE__ */ g("h2", {
103
+ children: /* @__PURE__ */ v("h2", {
103
104
  className: "font-semibold text-text-primary",
104
105
  children: [
105
- S("billing.addons.includedQuotas", "Included Quotas"),
106
+ w("billing.addons.includedQuotas", "Included Quotas"),
106
107
  " (",
107
- P.length,
108
+ I.length,
108
109
  ")"
109
110
  ]
110
111
  })
111
- }), /* @__PURE__ */ h("div", {
112
+ }), /* @__PURE__ */ _("div", {
112
113
  className: "p-4",
113
- children: P.length === 0 ? /* @__PURE__ */ h("p", {
114
+ children: I.length === 0 ? /* @__PURE__ */ _("p", {
114
115
  className: "text-sm text-text-secondary py-4 text-center",
115
- children: S("billing.addons.noQuotas", "No quotas associated with this addon.")
116
- }) : /* @__PURE__ */ h("div", {
116
+ children: w("billing.addons.noQuotas", "No quotas associated with this addon.")
117
+ }) : /* @__PURE__ */ _("div", {
117
118
  className: "space-y-3",
118
- children: P.map((e) => /* @__PURE__ */ g("div", {
119
+ children: I.map((e) => /* @__PURE__ */ v("div", {
119
120
  className: "flex items-center justify-between p-3 border border-border-subtle rounded-lg bg-bg-sunken/20",
120
- children: [/* @__PURE__ */ g("div", {
121
+ children: [/* @__PURE__ */ v("div", {
121
122
  className: "flex items-center gap-3",
122
- children: [/* @__PURE__ */ h("div", {
123
+ children: [/* @__PURE__ */ _("div", {
123
124
  className: "size-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center",
124
- children: /* @__PURE__ */ h("svg", {
125
+ children: /* @__PURE__ */ _("svg", {
125
126
  className: "size-4 text-primary",
126
127
  fill: "none",
127
128
  viewBox: "0 0 24 24",
128
129
  stroke: "currentColor",
129
- children: /* @__PURE__ */ h("path", {
130
+ children: /* @__PURE__ */ _("path", {
130
131
  strokeLinecap: "round",
131
132
  strokeLinejoin: "round",
132
133
  strokeWidth: 2,
133
134
  d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
134
135
  })
135
136
  })
136
- }), /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("p", {
137
+ }), /* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("p", {
137
138
  className: "font-medium text-text-primary",
138
139
  children: e.name
139
- }), /* @__PURE__ */ g("p", {
140
+ }), /* @__PURE__ */ v("p", {
140
141
  className: "text-xs text-text-secondary capitalize",
141
142
  children: ["Type: ", e.type]
142
143
  })] })]
143
- }), /* @__PURE__ */ g("div", {
144
+ }), /* @__PURE__ */ v("div", {
144
145
  className: "text-right",
145
- children: [/* @__PURE__ */ h("span", {
146
+ children: [/* @__PURE__ */ _("span", {
146
147
  className: "text-lg font-semibold text-status-success-text",
147
148
  children: e.totalValue.toLocaleString()
148
- }), /* @__PURE__ */ h("p", {
149
+ }), /* @__PURE__ */ _("p", {
149
150
  className: "text-xs text-text-secondary",
150
151
  children: "total"
151
152
  })]
@@ -154,117 +155,132 @@ var x = () => {
154
155
  })
155
156
  })]
156
157
  })]
157
- }), /* @__PURE__ */ g("div", {
158
+ }), /* @__PURE__ */ v("div", {
158
159
  className: "space-y-6",
159
- children: [/* @__PURE__ */ g("div", {
160
+ children: [/* @__PURE__ */ v("div", {
160
161
  className: "border border-border-subtle rounded-lg bg-bg-surface overflow-hidden",
161
- children: [/* @__PURE__ */ h("div", {
162
+ children: [/* @__PURE__ */ _("div", {
162
163
  className: "p-4 border-b border-border-subtle bg-bg-sunken/30",
163
- children: /* @__PURE__ */ h("h2", {
164
+ children: /* @__PURE__ */ _("h2", {
164
165
  className: "font-semibold text-text-primary",
165
- children: S("billing.addons.purchase", "Purchase")
166
+ children: w("billing.addons.purchase", "Purchase")
166
167
  })
167
- }), /* @__PURE__ */ g("div", {
168
+ }), /* @__PURE__ */ v("div", {
168
169
  className: "p-4 space-y-4",
169
170
  children: [
170
- /* @__PURE__ */ g("div", {
171
+ /* @__PURE__ */ v("div", {
171
172
  className: "flex items-center justify-between",
172
- children: [/* @__PURE__ */ h("span", {
173
+ children: [/* @__PURE__ */ _("span", {
173
174
  className: "text-sm text-text-secondary",
174
- children: S("billing.addons.price", "Price")
175
- }), /* @__PURE__ */ g("span", {
175
+ children: w("billing.addons.price", "Price")
176
+ }), /* @__PURE__ */ v("span", {
176
177
  className: "text-sm font-medium text-text-primary",
177
178
  children: [
178
- n(T.price, T.currency),
179
+ n(D.price, D.currency),
179
180
  " /",
180
181
  " ",
181
- r(T.duration)
182
+ r(D.duration)
182
183
  ]
183
184
  })]
184
185
  }),
185
- /* @__PURE__ */ g("div", {
186
+ /* @__PURE__ */ v("div", {
186
187
  className: "flex items-center justify-between",
187
- children: [/* @__PURE__ */ h("span", {
188
+ children: [/* @__PURE__ */ _("span", {
188
189
  className: "text-sm text-text-secondary",
189
- children: S("billing.addons.billingLabel", "Billing")
190
- }), /* @__PURE__ */ h("span", {
190
+ children: w("billing.addons.billingLabel", "Billing")
191
+ }), /* @__PURE__ */ _("span", {
191
192
  className: "text-sm font-medium text-text-primary capitalize",
192
- children: T.duration
193
+ children: D.duration
193
194
  })]
194
195
  }),
195
- /* @__PURE__ */ h("div", {
196
+ /* @__PURE__ */ _("div", {
196
197
  className: "pt-2 space-y-2",
197
- children: F ? /* @__PURE__ */ g("div", {
198
+ children: L ? /* @__PURE__ */ v("div", {
198
199
  className: "space-y-2",
199
- children: [/* @__PURE__ */ g("div", {
200
+ children: [/* @__PURE__ */ v("div", {
200
201
  className: "flex items-center justify-between gap-2",
201
- children: [/* @__PURE__ */ g("div", {
202
+ children: [/* @__PURE__ */ v("div", {
202
203
  className: "flex items-center gap-1",
203
204
  children: [
204
- /* @__PURE__ */ h("button", {
205
+ /* @__PURE__ */ _("button", {
205
206
  type: "button",
206
- onClick: () => j(T.id, I - 1),
207
+ onClick: () => N(D.id, R - 1),
207
208
  className: "p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors",
208
- children: /* @__PURE__ */ h(u, { className: "size-4" })
209
+ children: /* @__PURE__ */ _(f, { className: "size-4" })
209
210
  }),
210
- /* @__PURE__ */ h("span", {
211
+ /* @__PURE__ */ _("span", {
211
212
  className: "w-10 text-center font-medium",
212
- children: I
213
+ children: R
213
214
  }),
214
- /* @__PURE__ */ h("button", {
215
+ /* @__PURE__ */ _("button", {
215
216
  type: "button",
216
- onClick: () => j(T.id, I + 1),
217
+ onClick: () => N(D.id, R + 1),
217
218
  className: "p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors",
218
- children: /* @__PURE__ */ h(d, { className: "size-4" })
219
+ children: /* @__PURE__ */ _(p, { className: "size-4" })
219
220
  })
220
221
  ]
221
- }), /* @__PURE__ */ h("button", {
222
+ }), /* @__PURE__ */ _("button", {
222
223
  type: "button",
223
- onClick: () => A(T.id),
224
+ onClick: () => M(D.id),
224
225
  className: "p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors",
225
226
  title: "Remove from cart",
226
- children: /* @__PURE__ */ h(p, { className: "size-4" })
227
+ children: /* @__PURE__ */ _(h, { className: "size-4" })
227
228
  })]
228
- }), /* @__PURE__ */ h("button", {
229
+ }), /* @__PURE__ */ _("button", {
229
230
  type: "button",
230
- onClick: () => w("/checkout/addons"),
231
+ onClick: () => E("/checkout/addons"),
231
232
  className: "w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors",
232
233
  children: "Proceed to Checkout"
233
234
  })]
234
- }) : /* @__PURE__ */ g("button", {
235
+ }) : B ? /* @__PURE__ */ v("div", {
236
+ className: "space-y-3",
237
+ "data-testid": "addon-paid-plan-required",
238
+ children: [/* @__PURE__ */ v("p", {
239
+ className: "flex items-start gap-2 text-sm text-text-secondary",
240
+ children: [/* @__PURE__ */ _(d, {
241
+ className: "mt-0.5 size-4 shrink-0 text-status-warning-text",
242
+ "aria-hidden": "true"
243
+ }), w("billing.addons.paidPlanRequiredBody", "Add-ons extend a paid subscription. You are on the Free plan for this product, so upgrade to a paid plan first, then come back to add extra capacity.")]
244
+ }), /* @__PURE__ */ _("button", {
245
+ type: "button",
246
+ onClick: () => E("/plans"),
247
+ className: "w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors",
248
+ children: w("billing.addons.viewPlans", "View plans")
249
+ })]
250
+ }) : /* @__PURE__ */ v("button", {
235
251
  type: "button",
236
- onClick: () => k(T),
252
+ onClick: () => j(D),
237
253
  className: "w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-2",
238
- children: [/* @__PURE__ */ h(f, { className: "size-4" }), "Add to Cart"]
254
+ children: [/* @__PURE__ */ _(m, { className: "size-4" }), "Add to Cart"]
239
255
  })
240
256
  })
241
257
  ]
242
258
  })]
243
- }), /* @__PURE__ */ h("button", {
259
+ }), /* @__PURE__ */ _("button", {
244
260
  type: "button",
245
- onClick: () => w("/addons"),
261
+ onClick: () => E("/addons"),
246
262
  className: "w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors",
247
263
  children: "Back to Addons"
248
264
  })]
249
265
  })]
250
266
  })
251
267
  ]
252
- }) : /* @__PURE__ */ h("div", {
268
+ }) : /* @__PURE__ */ _("div", {
253
269
  className: "flex items-center justify-center h-full min-h-[400px]",
254
- children: /* @__PURE__ */ g("div", {
270
+ children: /* @__PURE__ */ v("div", {
255
271
  className: "text-center space-y-4",
256
272
  children: [
257
- /* @__PURE__ */ h("h2", {
273
+ /* @__PURE__ */ _("h2", {
258
274
  className: "text-lg font-semibold text-text-primary",
259
- children: S("billing.addons.addonNotFound", "Addon not found")
275
+ children: w("billing.addons.addonNotFound", "Addon not found")
260
276
  }),
261
- /* @__PURE__ */ h("p", {
277
+ /* @__PURE__ */ _("p", {
262
278
  className: "text-sm text-text-secondary",
263
- children: S("billing.addons.addonNotFoundMessage", "The addon you're looking for doesn't exist or has been removed.")
279
+ children: w("billing.addons.addonNotFoundMessage", "The addon you're looking for doesn't exist or has been removed.")
264
280
  }),
265
- /* @__PURE__ */ h("button", {
281
+ /* @__PURE__ */ _("button", {
266
282
  type: "button",
267
- onClick: () => w("/addons"),
283
+ onClick: () => E("/addons"),
268
284
  className: "px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors",
269
285
  children: "Back to Addons"
270
286
  })
@@ -273,6 +289,6 @@ var x = () => {
273
289
  });
274
290
  };
275
291
  //#endregion
276
- export { x as AddonBrowseDetailPage };
292
+ export { C as AddonBrowseDetailPage };
277
293
 
278
294
  //# sourceMappingURL=AddonBrowseDetailPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AddonBrowseDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonBrowseDetailPage.tsx"],"sourcesContent":["/**\n * Addons Module - Addon Browse Detail Page (User-facing)\n * View addon details and add to cart. No edit/toggle actions.\n */\n\nimport { useMemo, type FC } from 'react';\nimport { useParams } from 'react-router';\nimport { ShoppingCart, Plus, Minus, Trash2 } from 'lucide-react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useAddon } from '../hooks';\nimport { useAddonCartStore } from '../store/addonCartStore';\nimport { PageHeader, ServerError } from '../../../shared/components';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n isServerError,\n} from '../../../shared/utils';\nimport type { SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n type: string;\n}\n\nfunction combineQuotas(features: SubscriptionFeatures[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limits = feature.quota.limits as { value?: number; type?: string } | null;\n const limitValue = limits?.value ?? 0;\n const limitType = limits?.type ?? 'unknown';\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n type: limitType,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const AddonBrowseDetailPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { addonId } = useParams<{ addonId: string }>();\n const navigateTo = useBillingNavigate();\n const { addon, isLoading, error, refetch } = useAddon(addonId);\n const { addToCart, removeFromCart, updateQuantity, isInCart, getCartItem } = useAddonCartStore();\n\n const combinedQuotas = useMemo(() => combineQuotas(addon?.features || []), [addon?.features]);\n const inCart = addon ? isInCart(addon.id) : false;\n const cartItem = addon ? getCartItem(addon.id) : undefined;\n const cartQuantity = cartItem?.quantity || 0;\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-24 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n <div className=\"h-64 bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n );\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.addons.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.addons.unableToLoadAddonDetails', 'Unable to load addon details.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!addon) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.addons.addonNotFound', 'Addon not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'billing.addons.addonNotFoundMessage',\n \"The addon you're looking for doesn't exist or has been removed.\"\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Back to Addons\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={addon.name}\n description={`${formatPlanDurationLabel(addon.duration)} addon`}\n />\n\n <PagePurpose>\n {tr(\n 'billing.addons.browseDetailPurpose',\n 'See exactly what this add-on costs and the extra capacity it gives you on top of your plan before you buy it. Add it to your cart from here when it fits your needs.'\n )}\n </PagePurpose>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Pricing Card — primary emphasis zone */}\n <EmphasisPanel className=\"overflow-hidden p-0\">\n <div className=\"p-4 border-b border-border-subtle\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.pricing', 'Pricing')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(addon.price, addon.currency)}\n </span>\n <span className=\"text-lg text-text-secondary\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {tr('billing.addons.billed', 'Billed')}{' '}\n {addon.duration === 'monthly'\n ? tr('billing.addons.everyMonth', 'every month')\n : tr('billing.addons.annually', 'annually')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Quotas Card */}\n <div className=\"border border-border-subtle rounded-lg bg-bg-surface overflow-hidden\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.includedQuotas', 'Included Quotas')} ({combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-4\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary py-4 text-center\">\n {tr('billing.addons.noQuotas', 'No quotas associated with this addon.')}\n </p>\n ) : (\n <div className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <div\n key={quota.quotaId}\n className=\"flex items-center justify-between p-3 border border-border-subtle rounded-lg bg-bg-sunken/20\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <svg\n className=\"size-4 text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n </div>\n <div>\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <p className=\"text-xs text-text-secondary capitalize\">\n Type: {quota.type}\n </p>\n </div>\n </div>\n <div className=\"text-right\">\n <span className=\"text-lg font-semibold text-status-success-text\">\n {quota.totalValue.toLocaleString()}\n </span>\n <p className=\"text-xs text-text-secondary\">total</p>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Purchase Card */}\n <div className=\"border border-border-subtle rounded-lg bg-bg-surface overflow-hidden\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.purchase', 'Purchase')}\n </h2>\n </div>\n <div className=\"p-4 space-y-4\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-secondary\">\n {tr('billing.addons.price', 'Price')}\n </span>\n <span className=\"text-sm font-medium text-text-primary\">\n {formatCurrency(addon.price, addon.currency)} /{' '}\n {formatPlanDuration(addon.duration)}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-secondary\">\n {tr('billing.addons.billingLabel', 'Billing')}\n </span>\n <span className=\"text-sm font-medium text-text-primary capitalize\">\n {addon.duration}\n </span>\n </div>\n\n <div className=\"pt-2 space-y-2\">\n {inCart ? (\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-10 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('/checkout/addons')}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Proceed to Checkout\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-2\"\n >\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </button>\n )}\n </div>\n </div>\n </div>\n\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Back to Addons\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AddonBrowseDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/addons/pages/AddonBrowseDetailPage.tsx"],"sourcesContent":["/**\n * Addons Module - Addon Browse Detail Page (User-facing)\n * View addon details and add to cart. No edit/toggle actions.\n */\n\nimport { useMemo, type FC } from 'react';\nimport { useParams } from 'react-router';\nimport { ShoppingCart, Plus, Minus, Trash2, Lock } from 'lucide-react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useAddon, useAddonPurchaseEligibility } from '../hooks';\nimport { useAddonCartStore } from '../store/addonCartStore';\nimport { PageHeader, ServerError } from '../../../shared/components';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n isServerError,\n} from '../../../shared/utils';\nimport type { SubscriptionFeatures } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n type: string;\n}\n\nfunction combineQuotas(features: SubscriptionFeatures[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limits = feature.quota.limits as { value?: number; type?: string } | null;\n const limitValue = limits?.value ?? 0;\n const limitType = limits?.type ?? 'unknown';\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n type: limitType,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const AddonBrowseDetailPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { addonId } = useParams<{ addonId: string }>();\n const navigateTo = useBillingNavigate();\n const { addon, isLoading, error, refetch } = useAddon(addonId);\n const { addToCart, removeFromCart, updateQuantity, isInCart, getCartItem } = useAddonCartStore();\n\n const combinedQuotas = useMemo(() => combineQuotas(addon?.features || []), [addon?.features]);\n const inCart = addon ? isInCart(addon.id) : false;\n const cartItem = addon ? getCartItem(addon.id) : undefined;\n const cartQuantity = cartItem?.quantity || 0;\n\n // BOFF-9889: add-ons require a paid subscription to their product.\n const eligibilityIds = useMemo(() => (addon ? [addon.id] : []), [addon]);\n const { isBlocked } = useAddonPurchaseEligibility(eligibilityIds);\n const purchaseBlocked = addon ? isBlocked(addon.id) : false;\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-24 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n <div className=\"h-64 bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n );\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.addons.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.addons.unableToLoadAddonDetails', 'Unable to load addon details.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!addon) {\n return (\n <div className=\"flex items-center justify-center h-full min-h-[400px]\">\n <div className=\"text-center space-y-4\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.addons.addonNotFound', 'Addon not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'billing.addons.addonNotFoundMessage',\n \"The addon you're looking for doesn't exist or has been removed.\"\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Back to Addons\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={addon.name}\n description={`${formatPlanDurationLabel(addon.duration)} addon`}\n />\n\n <PagePurpose>\n {tr(\n 'billing.addons.browseDetailPurpose',\n 'See exactly what this add-on costs and the extra capacity it gives you on top of your plan before you buy it. Add it to your cart from here when it fits your needs.'\n )}\n </PagePurpose>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Pricing Card — primary emphasis zone */}\n <EmphasisPanel className=\"overflow-hidden p-0\">\n <div className=\"p-4 border-b border-border-subtle\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.pricing', 'Pricing')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(addon.price, addon.currency)}\n </span>\n <span className=\"text-lg text-text-secondary\">\n / {formatPlanDuration(addon.duration)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {tr('billing.addons.billed', 'Billed')}{' '}\n {addon.duration === 'monthly'\n ? tr('billing.addons.everyMonth', 'every month')\n : tr('billing.addons.annually', 'annually')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Quotas Card */}\n <div className=\"border border-border-subtle rounded-lg bg-bg-surface overflow-hidden\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.includedQuotas', 'Included Quotas')} ({combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-4\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary py-4 text-center\">\n {tr('billing.addons.noQuotas', 'No quotas associated with this addon.')}\n </p>\n ) : (\n <div className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <div\n key={quota.quotaId}\n className=\"flex items-center justify-between p-3 border border-border-subtle rounded-lg bg-bg-sunken/20\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"size-8 rounded-full bg-action-primary-bg/10 flex items-center justify-center\">\n <svg\n className=\"size-4 text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n </div>\n <div>\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <p className=\"text-xs text-text-secondary capitalize\">\n Type: {quota.type}\n </p>\n </div>\n </div>\n <div className=\"text-right\">\n <span className=\"text-lg font-semibold text-status-success-text\">\n {quota.totalValue.toLocaleString()}\n </span>\n <p className=\"text-xs text-text-secondary\">total</p>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6\">\n {/* Purchase Card */}\n <div className=\"border border-border-subtle rounded-lg bg-bg-surface overflow-hidden\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">\n {tr('billing.addons.purchase', 'Purchase')}\n </h2>\n </div>\n <div className=\"p-4 space-y-4\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-secondary\">\n {tr('billing.addons.price', 'Price')}\n </span>\n <span className=\"text-sm font-medium text-text-primary\">\n {formatCurrency(addon.price, addon.currency)} /{' '}\n {formatPlanDuration(addon.duration)}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm text-text-secondary\">\n {tr('billing.addons.billingLabel', 'Billing')}\n </span>\n <span className=\"text-sm font-medium text-text-primary capitalize\">\n {addon.duration}\n </span>\n </div>\n\n <div className=\"pt-2 space-y-2\">\n {inCart ? (\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, cartQuantity - 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Minus className=\"size-4\" />\n </button>\n <span className=\"w-10 text-center font-medium\">{cartQuantity}</span>\n <button\n type=\"button\"\n onClick={() => updateQuantity(addon.id, cartQuantity + 1)}\n className=\"p-1.5 rounded border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <Plus className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => removeFromCart(addon.id)}\n className=\"p-1.5 rounded text-status-error-text hover:bg-status-error-bg/10 transition-colors\"\n title=\"Remove from cart\"\n >\n <Trash2 className=\"size-4\" />\n </button>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('/checkout/addons')}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n Proceed to Checkout\n </button>\n </div>\n ) : purchaseBlocked ? (\n <div className=\"space-y-3\" data-testid=\"addon-paid-plan-required\">\n <p className=\"flex items-start gap-2 text-sm text-text-secondary\">\n <Lock\n className=\"mt-0.5 size-4 shrink-0 text-status-warning-text\"\n aria-hidden=\"true\"\n />\n {tr(\n 'billing.addons.paidPlanRequiredBody',\n 'Add-ons extend a paid subscription. You are on the Free plan for this product, so upgrade to a paid plan first, then come back to add extra capacity.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n {tr('billing.addons.viewPlans', 'View plans')}\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => addToCart(addon)}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors flex items-center justify-center gap-2\"\n >\n <ShoppingCart className=\"size-4\" />\n Add to Cart\n </button>\n )}\n </div>\n </div>\n </div>\n\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/addons')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-md hover:bg-bg-sunken transition-colors\"\n >\n Back to Addons\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}