@burdenoff/microfe-billing 2026.802.1 → 2026.805.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.
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +122 -121
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js +156 -155
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/shared/utils/format.js +14 -11
- package/dist/billing/shared/utils/format.js.map +1 -1
- package/dist/billing/shared/utils/planFeatureQuantity.js +25 -1
- package/dist/billing/shared/utils/planFeatureQuantity.js.map +1 -1
- package/dist/generated/global-operations.js +1 -0
- package/dist/generated/global-operations.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,198 +1,199 @@
|
|
|
1
1
|
import { ServerError as e } from "../../../shared/components/ServerError.js";
|
|
2
2
|
import "../../../shared/components/index.js";
|
|
3
|
-
import { formatCurrency as t, formatPlanDuration as n, formatPlanDurationLabel as r } from "../../../shared/utils/format.js";
|
|
4
|
-
import { isServerError as
|
|
5
|
-
import { getPlanFeatureQuantity as
|
|
3
|
+
import { formatCurrency as t, formatPlanDuration as n, formatPlanDurationLabel as r, formatQuotaValue as i } from "../../../shared/utils/format.js";
|
|
4
|
+
import { isServerError as a } from "../../../shared/utils/error.js";
|
|
5
|
+
import { getPlanFeatureQuantity as o, getPlanFeatureUnit as s, getQuotaDisplayName as c } from "../../../shared/utils/planFeatureQuantity.js";
|
|
6
6
|
import "../../../shared/utils/index.js";
|
|
7
|
-
import { useBillingNavigate as
|
|
8
|
-
import { usePlan as
|
|
7
|
+
import { useBillingNavigate as l } from "../../../hooks/useBillingNavigate.js";
|
|
8
|
+
import { usePlan as u } from "../hooks/usePlans.js";
|
|
9
9
|
import "../hooks/index.js";
|
|
10
|
-
import { useMemo as
|
|
11
|
-
import { useParams as
|
|
12
|
-
import { Fragment as
|
|
13
|
-
import { useI18n as
|
|
14
|
-
import { EmphasisPanel as
|
|
10
|
+
import { useMemo as d } from "react";
|
|
11
|
+
import { useParams as f } from "react-router";
|
|
12
|
+
import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
13
|
+
import { useI18n as g } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
14
|
+
import { EmphasisPanel as _, PagePurpose as v, ShowMore as y } from "@burdenoff/fe-libs/ui";
|
|
15
15
|
//#region src/billing/modules/plans/pages/PlanBrowseDetailPage.tsx
|
|
16
|
-
var
|
|
16
|
+
var b = ({ when: e, collapsedHeight: t, children: n }) => e ? /* @__PURE__ */ m(y, {
|
|
17
17
|
collapsedHeight: t,
|
|
18
18
|
children: n
|
|
19
|
-
}) : /* @__PURE__ */
|
|
20
|
-
function
|
|
19
|
+
}) : /* @__PURE__ */ m(p, { children: n });
|
|
20
|
+
function x(e) {
|
|
21
21
|
let t = /* @__PURE__ */ new Map();
|
|
22
22
|
for (let n of e) {
|
|
23
23
|
if (!n.quota) continue;
|
|
24
|
-
let e = n.quota.id, r = n.quota.limits?.type ?? "unknown", i =
|
|
25
|
-
|
|
24
|
+
let e = n.quota.id, r = n.quota.limits?.type ?? "unknown", i = o(n), a = t.get(e);
|
|
25
|
+
a ? a.totalValue += i : t.set(e, {
|
|
26
26
|
quotaId: e,
|
|
27
|
-
name: n
|
|
27
|
+
name: c(n),
|
|
28
28
|
quotaType: n.quota.quotaType ?? "",
|
|
29
29
|
limitType: r,
|
|
30
|
-
totalValue: i
|
|
30
|
+
totalValue: i,
|
|
31
|
+
unit: s(n)
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
return Array.from(t.values());
|
|
34
35
|
}
|
|
35
|
-
var
|
|
36
|
-
let { t:
|
|
37
|
-
let n =
|
|
36
|
+
var S = () => {
|
|
37
|
+
let { t: o } = g(), s = (e, t) => {
|
|
38
|
+
let n = o(e);
|
|
38
39
|
return n === e ? t : n;
|
|
39
|
-
}, { planId:
|
|
40
|
-
return
|
|
40
|
+
}, { planId: c } = f(), p = l(), { plan: y, isLoading: S, error: C, refetch: w } = u(c), T = d(() => x(y?.features || []), [y?.features]);
|
|
41
|
+
return S ? /* @__PURE__ */ h("div", {
|
|
41
42
|
className: "space-y-6 px-6 lg:px-8 py-6",
|
|
42
|
-
children: [/* @__PURE__ */
|
|
43
|
+
children: [/* @__PURE__ */ h("div", {
|
|
43
44
|
className: "flex items-center gap-4",
|
|
44
|
-
children: [/* @__PURE__ */
|
|
45
|
-
}), /* @__PURE__ */
|
|
45
|
+
children: [/* @__PURE__ */ m("div", { className: "size-10 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ m("div", { className: "h-8 w-48 bg-bg-sunken animate-pulse rounded" })]
|
|
46
|
+
}), /* @__PURE__ */ h("div", {
|
|
46
47
|
className: "grid grid-cols-1 lg:grid-cols-3 gap-6",
|
|
47
|
-
children: [/* @__PURE__ */
|
|
48
|
+
children: [/* @__PURE__ */ m("div", {
|
|
48
49
|
className: "lg:col-span-2 space-y-6",
|
|
49
|
-
children: /* @__PURE__ */
|
|
50
|
+
children: /* @__PURE__ */ h("div", {
|
|
50
51
|
className: "border border-border-subtle rounded-lg p-6 space-y-4",
|
|
51
|
-
children: [/* @__PURE__ */
|
|
52
|
+
children: [/* @__PURE__ */ m("div", { className: "h-6 w-32 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ m("div", { className: "h-4 w-full bg-bg-sunken animate-pulse rounded" })]
|
|
52
53
|
})
|
|
53
|
-
}), /* @__PURE__ */
|
|
54
|
+
}), /* @__PURE__ */ h("div", {
|
|
54
55
|
className: "border border-border-subtle rounded-lg p-6 space-y-4",
|
|
55
|
-
children: [/* @__PURE__ */
|
|
56
|
+
children: [/* @__PURE__ */ m("div", { className: "h-6 w-24 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ m("div", { className: "h-10 w-full bg-bg-sunken animate-pulse rounded" })]
|
|
56
57
|
})]
|
|
57
58
|
})]
|
|
58
|
-
}) :
|
|
59
|
+
}) : C && a(C) ? /* @__PURE__ */ m("div", {
|
|
59
60
|
className: "p-6",
|
|
60
|
-
children: /* @__PURE__ */
|
|
61
|
-
title:
|
|
62
|
-
message:
|
|
63
|
-
onRetry: () =>
|
|
61
|
+
children: /* @__PURE__ */ m(e, {
|
|
62
|
+
title: s("billing.plans.serverUnavailable", "Server Unavailable"),
|
|
63
|
+
message: s("billing.plans.unableToLoadPlanDetails", "Unable to load plan details."),
|
|
64
|
+
onRetry: () => w(),
|
|
64
65
|
showRetry: !0
|
|
65
66
|
})
|
|
66
|
-
}) :
|
|
67
|
+
}) : y ? /* @__PURE__ */ h("div", {
|
|
67
68
|
className: "space-y-6 px-6 lg:px-8 py-6",
|
|
68
|
-
children: [/* @__PURE__ */
|
|
69
|
+
children: [/* @__PURE__ */ h("div", {
|
|
69
70
|
className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
70
|
-
children: [/* @__PURE__ */
|
|
71
|
+
children: [/* @__PURE__ */ h("div", {
|
|
71
72
|
className: "flex items-start gap-4",
|
|
72
|
-
children: [/* @__PURE__ */
|
|
73
|
+
children: [/* @__PURE__ */ m("button", {
|
|
73
74
|
type: "button",
|
|
74
|
-
onClick: () =>
|
|
75
|
+
onClick: () => p("/plans"),
|
|
75
76
|
className: "p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors",
|
|
76
|
-
children: /* @__PURE__ */
|
|
77
|
+
children: /* @__PURE__ */ m("svg", {
|
|
77
78
|
className: "size-5",
|
|
78
79
|
fill: "none",
|
|
79
80
|
viewBox: "0 0 24 24",
|
|
80
81
|
stroke: "currentColor",
|
|
81
|
-
children: /* @__PURE__ */
|
|
82
|
+
children: /* @__PURE__ */ m("path", {
|
|
82
83
|
strokeLinecap: "round",
|
|
83
84
|
strokeLinejoin: "round",
|
|
84
85
|
strokeWidth: 2,
|
|
85
86
|
d: "M15 19l-7-7 7-7"
|
|
86
87
|
})
|
|
87
88
|
})
|
|
88
|
-
}), /* @__PURE__ */
|
|
89
|
-
/* @__PURE__ */
|
|
89
|
+
}), /* @__PURE__ */ h("div", { children: [
|
|
90
|
+
/* @__PURE__ */ m("h1", {
|
|
90
91
|
className: "text-2xl md:text-3xl font-semibold tracking-tight text-text-primary",
|
|
91
|
-
children:
|
|
92
|
+
children: y.name
|
|
92
93
|
}),
|
|
93
|
-
/* @__PURE__ */
|
|
94
|
+
/* @__PURE__ */ h("p", {
|
|
94
95
|
className: "text-sm text-text-secondary mt-1",
|
|
95
|
-
children: [r(
|
|
96
|
+
children: [r(y.duration), " billing plan"]
|
|
96
97
|
}),
|
|
97
|
-
/* @__PURE__ */
|
|
98
|
+
/* @__PURE__ */ m(v, {
|
|
98
99
|
className: "mt-3",
|
|
99
|
-
children:
|
|
100
|
+
children: s("billing.plans.browseDetailPurpose", "Everything this plan includes — its price, billing period and the full list of features and quotas — so you can confirm it fits before subscribing. Hit Subscribe when you are ready to check out.")
|
|
100
101
|
})
|
|
101
102
|
] })]
|
|
102
|
-
}), /* @__PURE__ */
|
|
103
|
+
}), /* @__PURE__ */ m("button", {
|
|
103
104
|
type: "button",
|
|
104
|
-
onClick: () =>
|
|
105
|
+
onClick: () => p(`/checkout/${y.id}`),
|
|
105
106
|
className: "px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors",
|
|
106
107
|
children: "Subscribe to Plan"
|
|
107
108
|
})]
|
|
108
|
-
}), /* @__PURE__ */
|
|
109
|
+
}), /* @__PURE__ */ h("div", {
|
|
109
110
|
className: "grid grid-cols-1 lg:grid-cols-3 gap-6",
|
|
110
|
-
children: [/* @__PURE__ */
|
|
111
|
+
children: [/* @__PURE__ */ h("div", {
|
|
111
112
|
className: "lg:col-span-2 space-y-6",
|
|
112
|
-
children: [/* @__PURE__ */
|
|
113
|
+
children: [/* @__PURE__ */ h(_, {
|
|
113
114
|
className: "overflow-hidden p-0",
|
|
114
|
-
children: [/* @__PURE__ */
|
|
115
|
+
children: [/* @__PURE__ */ m("div", {
|
|
115
116
|
className: "p-6 border-b border-border-seam",
|
|
116
|
-
children: /* @__PURE__ */
|
|
117
|
+
children: /* @__PURE__ */ m("h2", {
|
|
117
118
|
className: "text-lg font-semibold text-text-primary",
|
|
118
|
-
children:
|
|
119
|
+
children: s("billing.plans.pricing", "Pricing")
|
|
119
120
|
})
|
|
120
|
-
}), /* @__PURE__ */
|
|
121
|
+
}), /* @__PURE__ */ h("div", {
|
|
121
122
|
className: "p-6",
|
|
122
|
-
children: [/* @__PURE__ */
|
|
123
|
+
children: [/* @__PURE__ */ h("div", {
|
|
123
124
|
className: "flex items-baseline gap-1",
|
|
124
|
-
children: [/* @__PURE__ */
|
|
125
|
+
children: [/* @__PURE__ */ m("span", {
|
|
125
126
|
className: "text-4xl font-bold text-text-primary",
|
|
126
|
-
children: t(
|
|
127
|
-
}), /* @__PURE__ */
|
|
127
|
+
children: t(y.price, y.currency)
|
|
128
|
+
}), /* @__PURE__ */ h("span", {
|
|
128
129
|
className: "text-lg text-text-secondary",
|
|
129
|
-
children: ["/ ", n(
|
|
130
|
+
children: ["/ ", n(y.duration)]
|
|
130
131
|
})]
|
|
131
|
-
}), /* @__PURE__ */
|
|
132
|
+
}), /* @__PURE__ */ h("p", {
|
|
132
133
|
className: "text-sm text-text-secondary mt-2",
|
|
133
134
|
children: [
|
|
134
|
-
|
|
135
|
+
s("billing.plans.billed", "Billed"),
|
|
135
136
|
" ",
|
|
136
|
-
|
|
137
|
+
y.duration === "monthly" ? s("billing.plans.everyMonth", "every month") : s("billing.plans.annually", "annually")
|
|
137
138
|
]
|
|
138
139
|
})]
|
|
139
140
|
})]
|
|
140
|
-
}), /* @__PURE__ */
|
|
141
|
+
}), /* @__PURE__ */ h("div", {
|
|
141
142
|
className: "border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden",
|
|
142
|
-
children: [/* @__PURE__ */
|
|
143
|
+
children: [/* @__PURE__ */ m("div", {
|
|
143
144
|
className: "p-6 border-b border-border-seam bg-bg-sunken",
|
|
144
|
-
children: /* @__PURE__ */
|
|
145
|
+
children: /* @__PURE__ */ h("h2", {
|
|
145
146
|
className: "text-lg font-semibold text-text-primary",
|
|
146
147
|
children: [
|
|
147
|
-
|
|
148
|
+
s("billing.plans.featuresAndQuotas", "Features & Quotas"),
|
|
148
149
|
" (",
|
|
149
|
-
|
|
150
|
+
T.length,
|
|
150
151
|
")"
|
|
151
152
|
]
|
|
152
153
|
})
|
|
153
|
-
}), /* @__PURE__ */
|
|
154
|
+
}), /* @__PURE__ */ m("div", {
|
|
154
155
|
className: "p-6",
|
|
155
|
-
children:
|
|
156
|
+
children: T.length === 0 ? /* @__PURE__ */ m("p", {
|
|
156
157
|
className: "text-sm text-text-secondary",
|
|
157
|
-
children:
|
|
158
|
-
}) : /* @__PURE__ */
|
|
159
|
-
when:
|
|
158
|
+
children: s("billing.plans.noFeatures", "No features configured for this plan.")
|
|
159
|
+
}) : /* @__PURE__ */ m(b, {
|
|
160
|
+
when: T.length > 4,
|
|
160
161
|
collapsedHeight: 280,
|
|
161
|
-
children: /* @__PURE__ */
|
|
162
|
+
children: /* @__PURE__ */ m("ul", {
|
|
162
163
|
className: "space-y-3",
|
|
163
|
-
children:
|
|
164
|
+
children: T.map((e) => /* @__PURE__ */ h("li", {
|
|
164
165
|
className: "flex items-start gap-3 p-3 rounded-lg bg-bg-sunken",
|
|
165
|
-
children: [/* @__PURE__ */
|
|
166
|
+
children: [/* @__PURE__ */ m("div", {
|
|
166
167
|
className: "size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0",
|
|
167
|
-
children: /* @__PURE__ */
|
|
168
|
+
children: /* @__PURE__ */ m("svg", {
|
|
168
169
|
className: "size-4 text-status-success-text",
|
|
169
170
|
fill: "none",
|
|
170
171
|
viewBox: "0 0 24 24",
|
|
171
172
|
stroke: "currentColor",
|
|
172
|
-
children: /* @__PURE__ */
|
|
173
|
+
children: /* @__PURE__ */ m("path", {
|
|
173
174
|
strokeLinecap: "round",
|
|
174
175
|
strokeLinejoin: "round",
|
|
175
176
|
strokeWidth: 2,
|
|
176
177
|
d: "M5 13l4 4L19 7"
|
|
177
178
|
})
|
|
178
179
|
})
|
|
179
|
-
}), /* @__PURE__ */
|
|
180
|
+
}), /* @__PURE__ */ h("div", {
|
|
180
181
|
className: "flex-1",
|
|
181
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ h("div", {
|
|
182
183
|
className: "flex items-center justify-between",
|
|
183
|
-
children: [/* @__PURE__ */
|
|
184
|
+
children: [/* @__PURE__ */ m("p", {
|
|
184
185
|
className: "font-medium text-text-primary",
|
|
185
186
|
children: e.name
|
|
186
|
-
}), /* @__PURE__ */
|
|
187
|
+
}), /* @__PURE__ */ m("span", {
|
|
187
188
|
className: "font-semibold text-text-primary",
|
|
188
|
-
children: e.totalValue.
|
|
189
|
+
children: i(e.totalValue, e.unit)
|
|
189
190
|
})]
|
|
190
|
-
}), /* @__PURE__ */
|
|
191
|
+
}), /* @__PURE__ */ h("div", {
|
|
191
192
|
className: "flex items-center gap-2 mt-1",
|
|
192
|
-
children: [/* @__PURE__ */
|
|
193
|
+
children: [/* @__PURE__ */ h("span", {
|
|
193
194
|
className: "text-sm text-text-secondary capitalize",
|
|
194
195
|
children: ["Type: ", e.limitType]
|
|
195
|
-
}), e.quotaType && /* @__PURE__ */
|
|
196
|
+
}), e.quotaType && /* @__PURE__ */ m("span", {
|
|
196
197
|
className: "px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-secondary rounded",
|
|
197
198
|
children: e.quotaType
|
|
198
199
|
})]
|
|
@@ -203,76 +204,76 @@ var y = () => {
|
|
|
203
204
|
})
|
|
204
205
|
})]
|
|
205
206
|
})]
|
|
206
|
-
}), /* @__PURE__ */
|
|
207
|
+
}), /* @__PURE__ */ h("div", {
|
|
207
208
|
className: "space-y-6 lg:sticky lg:top-6 lg:self-start",
|
|
208
|
-
children: [/* @__PURE__ */
|
|
209
|
+
children: [/* @__PURE__ */ h("div", {
|
|
209
210
|
className: "border border-border-seam rounded-card bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)] overflow-hidden",
|
|
210
|
-
children: [/* @__PURE__ */
|
|
211
|
+
children: [/* @__PURE__ */ m("div", {
|
|
211
212
|
className: "p-6 border-b border-border-seam bg-bg-sunken",
|
|
212
|
-
children: /* @__PURE__ */
|
|
213
|
+
children: /* @__PURE__ */ m("h2", {
|
|
213
214
|
className: "text-lg font-semibold text-text-primary",
|
|
214
|
-
children:
|
|
215
|
+
children: s("billing.plans.subscribeTitle", "Subscribe")
|
|
215
216
|
})
|
|
216
|
-
}), /* @__PURE__ */
|
|
217
|
+
}), /* @__PURE__ */ h("div", {
|
|
217
218
|
className: "p-6 space-y-4",
|
|
218
219
|
children: [
|
|
219
|
-
/* @__PURE__ */
|
|
220
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
220
221
|
className: "text-sm text-text-secondary",
|
|
221
|
-
children:
|
|
222
|
-
}), /* @__PURE__ */
|
|
222
|
+
children: s("billing.plans.price", "Price")
|
|
223
|
+
}), /* @__PURE__ */ h("p", {
|
|
223
224
|
className: "text-sm font-medium text-text-primary mt-0.5",
|
|
224
225
|
children: [
|
|
225
|
-
t(
|
|
226
|
+
t(y.price, y.currency),
|
|
226
227
|
" / ",
|
|
227
|
-
n(
|
|
228
|
+
n(y.duration)
|
|
228
229
|
]
|
|
229
230
|
})] }),
|
|
230
|
-
/* @__PURE__ */
|
|
231
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
231
232
|
className: "text-sm text-text-secondary",
|
|
232
|
-
children:
|
|
233
|
-
}), /* @__PURE__ */
|
|
233
|
+
children: s("billing.plans.billingPeriod", "Billing Period")
|
|
234
|
+
}), /* @__PURE__ */ m("p", {
|
|
234
235
|
className: "text-sm text-text-primary mt-0.5",
|
|
235
|
-
children: r(
|
|
236
|
+
children: r(y.duration)
|
|
236
237
|
})] }),
|
|
237
|
-
/* @__PURE__ */
|
|
238
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
238
239
|
className: "text-sm text-text-secondary",
|
|
239
|
-
children:
|
|
240
|
-
}), /* @__PURE__ */
|
|
240
|
+
children: s("billing.plans.quotasIncluded", "Quotas Included")
|
|
241
|
+
}), /* @__PURE__ */ m("p", {
|
|
241
242
|
className: "text-sm font-medium text-text-primary mt-0.5",
|
|
242
|
-
children:
|
|
243
|
+
children: T.length
|
|
243
244
|
})] }),
|
|
244
|
-
/* @__PURE__ */
|
|
245
|
+
/* @__PURE__ */ m("button", {
|
|
245
246
|
type: "button",
|
|
246
|
-
onClick: () =>
|
|
247
|
+
onClick: () => p(`/checkout/${y.id}`),
|
|
247
248
|
className: "w-full px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors",
|
|
248
249
|
children: "Subscribe to Plan"
|
|
249
250
|
})
|
|
250
251
|
]
|
|
251
252
|
})]
|
|
252
|
-
}), /* @__PURE__ */
|
|
253
|
+
}), /* @__PURE__ */ m("button", {
|
|
253
254
|
type: "button",
|
|
254
|
-
onClick: () =>
|
|
255
|
+
onClick: () => p("/plans"),
|
|
255
256
|
className: "w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors",
|
|
256
257
|
children: "Back to Plans"
|
|
257
258
|
})]
|
|
258
259
|
})]
|
|
259
260
|
})]
|
|
260
|
-
}) : /* @__PURE__ */
|
|
261
|
+
}) : /* @__PURE__ */ m("div", {
|
|
261
262
|
className: "flex items-center justify-center h-full min-h-[400px]",
|
|
262
|
-
children: /* @__PURE__ */
|
|
263
|
+
children: /* @__PURE__ */ h("div", {
|
|
263
264
|
className: "text-center space-y-4",
|
|
264
265
|
children: [
|
|
265
|
-
/* @__PURE__ */
|
|
266
|
+
/* @__PURE__ */ m("h2", {
|
|
266
267
|
className: "text-lg font-semibold text-text-primary",
|
|
267
|
-
children:
|
|
268
|
+
children: s("billing.plans.planNotFound", "Plan not found")
|
|
268
269
|
}),
|
|
269
|
-
/* @__PURE__ */
|
|
270
|
+
/* @__PURE__ */ m("p", {
|
|
270
271
|
className: "text-sm text-text-secondary",
|
|
271
|
-
children:
|
|
272
|
+
children: C?.message || s("billing.plans.planNotFoundMessage", "The plan you are looking for does not exist.")
|
|
272
273
|
}),
|
|
273
|
-
/* @__PURE__ */
|
|
274
|
+
/* @__PURE__ */ m("button", {
|
|
274
275
|
type: "button",
|
|
275
|
-
onClick: () =>
|
|
276
|
+
onClick: () => p("/plans"),
|
|
276
277
|
className: "px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors",
|
|
277
278
|
children: "Back to Plans"
|
|
278
279
|
})
|
|
@@ -281,6 +282,6 @@ var y = () => {
|
|
|
281
282
|
});
|
|
282
283
|
};
|
|
283
284
|
//#endregion
|
|
284
|
-
export {
|
|
285
|
+
export { S as PlanBrowseDetailPage };
|
|
285
286
|
|
|
286
287
|
//# sourceMappingURL=PlanBrowseDetailPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlanBrowseDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanBrowseDetailPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Browse Detail Page (User-facing)\n * View plan details and subscribe. No edit/toggle actions.\n */\n\nimport { type FC, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n getPlanFeatureQuantity,\n isServerError,\n} from '../../../shared/utils';\nimport type { PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ShowMore, PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport type { ReactNode } from 'react';\n\n/** Wraps children in a ShowMore progressive-disclosure region only when `when` is true. */\nconst ShowMoreIf: FC<{ when: boolean; collapsedHeight?: number; children: ReactNode }> = ({\n when,\n collapsedHeight,\n children,\n}) => (when ? <ShowMore collapsedHeight={collapsedHeight}>{children}</ShowMore> : <>{children}</>);\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n quotaType: string;\n limitType: string;\n totalValue: number;\n}\n\nfunction combineQuotas(features: PlanFeature[]): 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 { type?: string; value?: number } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = getPlanFeatureQuantity(feature);\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 quotaType: feature.quota.quotaType ?? '',\n limitType,\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const PlanBrowseDetailPage: 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 { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const { plan, isLoading, error, refetch } = usePlan(planId);\n\n const combinedQuotas = useMemo(() => combineQuotas(plan?.features || []), [plan?.features]);\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-10 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\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.plans.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.plans.unableToLoadPlanDetails', 'Unable to load plan details.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!plan) {\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.plans.planNotFound', 'Plan not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {error?.message ||\n tr(\n 'billing.plans.planNotFoundMessage',\n 'The plan you are looking for does not exist.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <svg className=\"size-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan.name}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {formatPlanDurationLabel(plan.duration)} billing plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.plans.browseDetailPurpose',\n 'Everything this plan includes — its price, billing period and the full list of features and quotas — so you can confirm it fits before subscribing. Hit Subscribe when you are ready to check out.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\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-6 border-b border-border-seam\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.pricing', 'Pricing')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </span>\n <span className=\"text-lg text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {tr('billing.plans.billed', 'Billed')}{' '}\n {plan.duration === 'monthly'\n ? tr('billing.plans.everyMonth', 'every month')\n : tr('billing.plans.annually', 'annually')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Features Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.featuresAndQuotas', 'Features & Quotas')} (\n {combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-6\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.noFeatures', 'No features configured for this plan.')}\n </p>\n ) : (\n <ShowMoreIf when={combinedQuotas.length > 4} collapsedHeight={280}>\n <ul className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <li\n key={quota.quotaId}\n className=\"flex items-start gap-3 p-3 rounded-lg bg-bg-sunken\"\n >\n <div className=\"size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0\">\n <svg\n className=\"size-4 text-status-success-text\"\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=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <span className=\"font-semibold text-text-primary\">\n {quota.totalValue.toLocaleString()}\n </span>\n </div>\n <div className=\"flex items-center gap-2 mt-1\">\n <span className=\"text-sm text-text-secondary capitalize\">\n Type: {quota.limitType}\n </span>\n {quota.quotaType && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-secondary rounded\">\n {quota.quotaType}\n </span>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n </ShowMoreIf>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6 lg:sticky lg:top-6 lg:self-start\">\n {/* Subscribe Card — primary emphasis zone */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)] overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.subscribeTitle', 'Subscribe')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">{tr('billing.plans.price', 'Price')}</p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {formatCurrency(plan.price, plan.currency)} / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.billingPeriod', 'Billing Period')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatPlanDurationLabel(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.quotasIncluded', 'Quotas Included')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {combinedQuotas.length}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"w-full px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n </div>\n\n {/* Back */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlanBrowseDetailPage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlanBrowseDetailPage.tsx"],"sourcesContent":["/**\n * Plans Module - Plan Browse Detail Page (User-facing)\n * View plan details and subscribe. No edit/toggle actions.\n */\n\nimport { type FC, useMemo } from 'react';\nimport { useParams } from 'react-router';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { usePlan } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n formatQuotaValue,\n getPlanFeatureQuantity,\n getPlanFeatureUnit,\n getQuotaDisplayName,\n isServerError,\n} from '../../../shared/utils';\nimport type { PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ShowMore, PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport type { ReactNode } from 'react';\n\n/** Wraps children in a ShowMore progressive-disclosure region only when `when` is true. */\nconst ShowMoreIf: FC<{ when: boolean; collapsedHeight?: number; children: ReactNode }> = ({\n when,\n collapsedHeight,\n children,\n}) => (when ? <ShowMore collapsedHeight={collapsedHeight}>{children}</ShowMore> : <>{children}</>);\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n quotaType: string;\n limitType: string;\n totalValue: number;\n unit?: string;\n}\n\nfunction combineQuotas(features: PlanFeature[]): 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 { type?: string; value?: number } | null;\n const limitType = limits?.type ?? 'unknown';\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: getQuotaDisplayName(feature),\n quotaType: feature.quota.quotaType ?? '',\n limitType,\n totalValue: limitValue,\n unit: getPlanFeatureUnit(feature),\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const PlanBrowseDetailPage: 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 { planId } = useParams<{ planId: string }>();\n const navigateTo = useBillingNavigate();\n const { plan, isLoading, error, refetch } = usePlan(planId);\n\n const combinedQuotas = useMemo(() => combineQuotas(plan?.features || []), [plan?.features]);\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"size-10 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-32 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-4 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\n </div>\n <div className=\"border border-border-subtle rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"h-10 w-full bg-bg-sunken animate-pulse rounded\" />\n </div>\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.plans.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.plans.unableToLoadPlanDetails', 'Unable to load plan details.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n if (!plan) {\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.plans.planNotFound', 'Plan not found')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {error?.message ||\n tr(\n 'billing.plans.planNotFoundMessage',\n 'The plan you are looking for does not exist.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"p-2 -ml-2 text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-button transition-colors\"\n >\n <svg className=\"size-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n <div>\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-text-primary\">\n {plan.name}\n </h1>\n <p className=\"text-sm text-text-secondary mt-1\">\n {formatPlanDurationLabel(plan.duration)} billing plan\n </p>\n <PagePurpose className=\"mt-3\">\n {tr(\n 'billing.plans.browseDetailPurpose',\n 'Everything this plan includes — its price, billing period and the full list of features and quotas — so you can confirm it fits before subscribing. Hit Subscribe when you are ready to check out.'\n )}\n </PagePurpose>\n </div>\n </div>\n\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\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-6 border-b border-border-seam\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.pricing', 'Pricing')}\n </h2>\n </div>\n <div className=\"p-6\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold text-text-primary\">\n {formatCurrency(plan.price, plan.currency)}\n </span>\n <span className=\"text-lg text-text-secondary\">\n / {formatPlanDuration(plan.duration)}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {tr('billing.plans.billed', 'Billed')}{' '}\n {plan.duration === 'monthly'\n ? tr('billing.plans.everyMonth', 'every month')\n : tr('billing.plans.annually', 'annually')}\n </p>\n </div>\n </EmphasisPanel>\n\n {/* Features Card */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.featuresAndQuotas', 'Features & Quotas')} (\n {combinedQuotas.length})\n </h2>\n </div>\n <div className=\"p-6\">\n {combinedQuotas.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.noFeatures', 'No features configured for this plan.')}\n </p>\n ) : (\n <ShowMoreIf when={combinedQuotas.length > 4} collapsedHeight={280}>\n <ul className=\"space-y-3\">\n {combinedQuotas.map((quota) => (\n <li\n key={quota.quotaId}\n className=\"flex items-start gap-3 p-3 rounded-lg bg-bg-sunken\"\n >\n <div className=\"size-8 rounded-full bg-status-success-bg-subtle flex items-center justify-center shrink-0\">\n <svg\n className=\"size-4 text-status-success-text\"\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=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between\">\n <p className=\"font-medium text-text-primary\">{quota.name}</p>\n <span className=\"font-semibold text-text-primary\">\n {formatQuotaValue(quota.totalValue, quota.unit)}\n </span>\n </div>\n <div className=\"flex items-center gap-2 mt-1\">\n <span className=\"text-sm text-text-secondary capitalize\">\n Type: {quota.limitType}\n </span>\n {quota.quotaType && (\n <span className=\"px-2 py-0.5 text-xs font-medium bg-bg-sunken text-text-secondary rounded\">\n {quota.quotaType}\n </span>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n </ShowMoreIf>\n )}\n </div>\n </div>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-6 lg:sticky lg:top-6 lg:self-start\">\n {/* Subscribe Card — primary emphasis zone */}\n <div className=\"border border-border-seam rounded-card bg-bg-surface bg-accent-wash shadow-[var(--shadow-pop)] overflow-hidden\">\n <div className=\"p-6 border-b border-border-seam bg-bg-sunken\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.plans.subscribeTitle', 'Subscribe')}\n </h2>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <p className=\"text-sm text-text-secondary\">{tr('billing.plans.price', 'Price')}</p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {formatCurrency(plan.price, plan.currency)} / {formatPlanDuration(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.billingPeriod', 'Billing Period')}\n </p>\n <p className=\"text-sm text-text-primary mt-0.5\">\n {formatPlanDurationLabel(plan.duration)}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('billing.plans.quotasIncluded', 'Quotas Included')}\n </p>\n <p className=\"text-sm font-medium text-text-primary mt-0.5\">\n {combinedQuotas.length}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/checkout/${plan.id}`)}\n className=\"w-full px-4 py-2.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors\"\n >\n Subscribe to Plan\n </button>\n </div>\n </div>\n\n {/* Back */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/plans')}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors\"\n >\n Back to Plans\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|