@zuplo/zudoku-plugin-monetization 0.0.42 → 0.0.44
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/{PricingTable-BlcXx4-5.mjs → PricingTable-WkG2n7V-.mjs} +354 -79
- package/dist/index.mjs +419 -274
- package/dist/pricing-ui.d.mts +67 -1
- package/dist/pricing-ui.mjs +2 -2
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as formatDurationAdjective, E as formatDuration, O as formatDurationInterval, T as formatPrice, _ as formatPlanPrice, a as planHasDefaultTaxBehavior, b as sameEntitlementSet, c as getPlanPriceSchedule, d as PlanEntitlements, f as PlanPhaseHeader, l as PlanPriceTag, o as subscriptionTaxLegendSentence, p as EntitlementList, r as isCustomPlan, t as PricingTable, u as PlanPriceSchedule, w as formatMinorCurrencyAmount, x as categorizeRateCards, y as comparePlanEntitlements } from "./PricingTable-WkG2n7V-.mjs";
|
|
2
2
|
import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
|
|
3
|
-
import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
|
|
3
|
+
import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
|
|
4
4
|
import { Link, Outlet, useLocation, useNavigate, useSearchParams } from "zudoku/router";
|
|
5
5
|
import { useAuth, useZudoku } from "zudoku/hooks";
|
|
6
6
|
import { QueryClient, QueryClientProvider, queryOptions, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
|
|
@@ -9,6 +9,7 @@ import { createContext, use, useEffect, useMemo, useState } from "react";
|
|
|
9
9
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
|
|
11
11
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "zudoku/ui/Card";
|
|
12
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "zudoku/ui/Collapsible";
|
|
12
13
|
import { Separator } from "zudoku/ui/Separator";
|
|
13
14
|
import { Button as Button$1 } from "zudoku/ui/Button";
|
|
14
15
|
import { DismissibleAlert, DismissibleAlertAction } from "zudoku/ui/DismissibleAlert";
|
|
@@ -256,18 +257,34 @@ const formatBillingCycle = (duration) => {
|
|
|
256
257
|
* Plan summary shown on the checkout and plan-change confirmation pages: an
|
|
257
258
|
* avatar + name/description on the left and the headline price (or
|
|
258
259
|
* "Free" / "Pay as you go") plus tax and billing cadence on the right,
|
|
259
|
-
* followed by the plan's included entitlements.
|
|
260
|
+
* followed by the plan's included entitlements. Multi-phase ramp plans render
|
|
261
|
+
* a full-width per-phase price schedule beneath the title instead of the
|
|
262
|
+
* single right-column price.
|
|
260
263
|
*
|
|
261
264
|
* The price is derived from the plan's rate cards via {@link formatPlanPrice}
|
|
262
265
|
* and rendered in the plan's own billing cadence, so it stays correct for any
|
|
263
266
|
* cadence (e.g. `$2.99/hour`).
|
|
267
|
+
*
|
|
268
|
+
* Pass `collapsibleDetails` to hide the "What's included" section behind a
|
|
269
|
+
* collapsed-by-default toggle — used on the plan-change confirmation page where
|
|
270
|
+
* the current and new plan are stacked and the user opens each to compare. Pass
|
|
271
|
+
* `label` to render a small eyebrow above the name (e.g. "Current plan").
|
|
264
272
|
*/
|
|
265
|
-
const PlanSummaryCard = ({ plan, descriptionFallback, taxAmount, taxLabel, taxInclusive, units, entitlementsItemClassName }) => {
|
|
273
|
+
const PlanSummaryCard = ({ plan, label, descriptionFallback, taxAmount, taxLabel, taxInclusive, units, entitlementsItemClassName, collapsibleDetails = false }) => {
|
|
266
274
|
const priceLabel = formatPlanPrice(plan);
|
|
275
|
+
const schedule = getPlanPriceSchedule(plan);
|
|
267
276
|
const billingCycle = plan.billingCadence ? formatDuration(plan.billingCadence) : null;
|
|
277
|
+
const taxLine = taxAmount != null && /* @__PURE__ */ jsx("div", {
|
|
278
|
+
className: "text-sm font-normal mt-1",
|
|
279
|
+
children: taxInclusive ? `${formatMinorCurrencyAmount(taxAmount, plan.currency)} ${taxLabel} included` : `+ ${formatMinorCurrencyAmount(taxAmount, plan.currency)} ${taxLabel}`
|
|
280
|
+
});
|
|
281
|
+
const billedLine = billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
282
|
+
className: "text-sm text-muted-foreground font-normal",
|
|
283
|
+
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
284
|
+
});
|
|
268
285
|
return /* @__PURE__ */ jsxs(Card, {
|
|
269
286
|
className: "bg-muted/50",
|
|
270
|
-
children: [/* @__PURE__ */
|
|
287
|
+
children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsxs(CardTitle, {
|
|
271
288
|
className: "flex justify-between items-start",
|
|
272
289
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
273
290
|
className: "flex items-center gap-3",
|
|
@@ -276,43 +293,62 @@ const PlanSummaryCard = ({ plan, descriptionFallback, taxAmount, taxLabel, taxIn
|
|
|
276
293
|
children: plan.name.at(0)?.toUpperCase()
|
|
277
294
|
}), /* @__PURE__ */ jsxs("div", {
|
|
278
295
|
className: "flex flex-col",
|
|
279
|
-
children: [
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
296
|
+
children: [
|
|
297
|
+
label && /* @__PURE__ */ jsx("span", {
|
|
298
|
+
className: "text-xs font-medium text-muted-foreground uppercase tracking-wide",
|
|
299
|
+
children: label
|
|
300
|
+
}),
|
|
301
|
+
/* @__PURE__ */ jsx("span", {
|
|
302
|
+
className: "text-lg font-bold",
|
|
303
|
+
children: plan.name
|
|
304
|
+
}),
|
|
305
|
+
/* @__PURE__ */ jsx("span", {
|
|
306
|
+
className: "text-sm font-normal text-muted-foreground",
|
|
307
|
+
children: plan.description || descriptionFallback
|
|
308
|
+
})
|
|
309
|
+
]
|
|
286
310
|
})]
|
|
287
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
311
|
+
}), !schedule && /* @__PURE__ */ jsxs("div", {
|
|
288
312
|
className: "text-right",
|
|
289
313
|
children: [/* @__PURE__ */ jsx(PlanPriceTag, {
|
|
290
314
|
label: priceLabel,
|
|
291
315
|
currency: plan.currency,
|
|
292
316
|
size: "lg",
|
|
293
317
|
description: true
|
|
294
|
-
}), priceLabel.type === "priced" && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
295
|
-
className: "text-sm font-normal mt-1",
|
|
296
|
-
children: taxInclusive ? `${formatMinorCurrencyAmount(taxAmount, plan.currency)} ${taxLabel} included` : `+ ${formatMinorCurrencyAmount(taxAmount, plan.currency)} ${taxLabel}`
|
|
297
|
-
}), billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
298
|
-
className: "text-sm text-muted-foreground font-normal",
|
|
299
|
-
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
300
|
-
})] })]
|
|
318
|
+
}), priceLabel.type === "priced" && /* @__PURE__ */ jsxs(Fragment$1, { children: [taxLine, billedLine] })]
|
|
301
319
|
})]
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
/* @__PURE__ */ jsx(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
/* @__PURE__ */
|
|
320
|
+
}), schedule && /* @__PURE__ */ jsxs("div", {
|
|
321
|
+
className: "mt-3 font-normal",
|
|
322
|
+
children: [/* @__PURE__ */ jsx(PlanPriceSchedule, {
|
|
323
|
+
schedule,
|
|
324
|
+
currency: plan.currency,
|
|
325
|
+
billingCadence: plan.billingCadence
|
|
326
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
327
|
+
className: "text-right",
|
|
328
|
+
children: [taxLine, billedLine]
|
|
329
|
+
})]
|
|
330
|
+
})] }), /* @__PURE__ */ jsxs(CardContent, { children: [/* @__PURE__ */ jsx(Separator, {}), collapsibleDetails ? /* @__PURE__ */ jsxs(Collapsible, { children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
331
|
+
className: "group flex w-full items-center justify-between text-sm font-medium mt-3",
|
|
332
|
+
children: ["Plan details", /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })]
|
|
333
|
+
}), /* @__PURE__ */ jsx(CollapsibleContent, {
|
|
334
|
+
className: "mt-3",
|
|
335
|
+
children: /* @__PURE__ */ jsx(PlanEntitlements, {
|
|
309
336
|
phases: plan.phases,
|
|
310
337
|
currency: plan.currency,
|
|
311
338
|
billingCadence: plan.billingCadence,
|
|
312
339
|
units,
|
|
313
340
|
itemClassName: entitlementsItemClassName
|
|
314
341
|
})
|
|
315
|
-
] })
|
|
342
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
343
|
+
className: "text-sm font-medium mb-3 mt-3",
|
|
344
|
+
children: "What's included:"
|
|
345
|
+
}), /* @__PURE__ */ jsx(PlanEntitlements, {
|
|
346
|
+
phases: plan.phases,
|
|
347
|
+
currency: plan.currency,
|
|
348
|
+
billingCadence: plan.billingCadence,
|
|
349
|
+
units,
|
|
350
|
+
itemClassName: entitlementsItemClassName
|
|
351
|
+
})] })] })]
|
|
316
352
|
});
|
|
317
353
|
};
|
|
318
354
|
//#endregion
|
|
@@ -787,76 +823,121 @@ const getSubscriptionPlanView = (subscription, options) => {
|
|
|
787
823
|
};
|
|
788
824
|
/** Whether a resolved view has any entitlements to render. */
|
|
789
825
|
const hasSubscriptionEntitlements = (view) => view.usingItems ? view.entitlements.quotas.length > 0 || view.entitlements.features.length > 0 : view.fallbackPhases.some((p) => p.rateCards?.some((rc) => rc.entitlementTemplate));
|
|
790
|
-
|
|
791
|
-
|
|
826
|
+
const phaseStatus = (phase, now) => {
|
|
827
|
+
if (phase.activeTo && new Date(phase.activeTo).getTime() < now) return "past";
|
|
828
|
+
if (new Date(phase.activeFrom).getTime() > now) return "future";
|
|
829
|
+
return "current";
|
|
830
|
+
};
|
|
792
831
|
/**
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
* plan's phases. Centralizes the "items, else fall back to plan phases" branch
|
|
797
|
-
* shared by the subscription details page and the Switch Plan baseline.
|
|
798
|
-
*
|
|
799
|
-
* Renders `null` when there's nothing to show, so callers can wrap it with
|
|
800
|
-
* {@link hasSubscriptionEntitlements} (for a section header / border) without
|
|
801
|
-
* leaving an empty container behind.
|
|
832
|
+
* Rate cards for one subscription phase: its own provisioned items (the
|
|
833
|
+
* authoritative quotas/fees), falling back to the catalog plan phase with the
|
|
834
|
+
* same `key` when a phase (typically a future one) hasn't been provisioned yet.
|
|
802
835
|
*/
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
836
|
+
const phaseRateCards = (plan, phase) => phase.items.length > 0 ? subscriptionItemsToRateCards(phase.items) : plan.phases?.find((p) => p.key === phase.key)?.rateCards ?? [];
|
|
837
|
+
const phasesByActiveFrom = (subscription) => [...subscription.phases].sort((a, b) => new Date(a.activeFrom).getTime() - new Date(b.activeFrom).getTime());
|
|
838
|
+
/**
|
|
839
|
+
* A clean ISO-8601 duration between two instants, preferring whole calendar
|
|
840
|
+
* units (years/months/weeks) over raw days so it formats the same way catalog
|
|
841
|
+
* plan durations do (e.g. `P1W` → "week", `P3M` → "3 months") rather than as
|
|
842
|
+
* "7 days" / "90 days". Returns `undefined` for a non-positive span.
|
|
843
|
+
*/
|
|
844
|
+
const isoDurationBetween = (from, to) => {
|
|
845
|
+
const a = new Date(from);
|
|
846
|
+
const b = new Date(to);
|
|
847
|
+
const ms = b.getTime() - a.getTime();
|
|
848
|
+
if (!(ms > 0)) return void 0;
|
|
849
|
+
let months = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
|
|
850
|
+
const anchor = new Date(a);
|
|
851
|
+
anchor.setMonth(a.getMonth() + months);
|
|
852
|
+
if (anchor.getTime() > b.getTime()) months -= 1;
|
|
853
|
+
const exact = new Date(a);
|
|
854
|
+
exact.setMonth(a.getMonth() + months);
|
|
855
|
+
if (months >= 1 && exact.getTime() === b.getTime()) return months % 12 === 0 ? `P${months / 12}Y` : `P${months}M`;
|
|
856
|
+
const days = Math.round(ms / 864e5);
|
|
857
|
+
if (days >= 7 && days % 7 === 0) return `P${days / 7}W`;
|
|
858
|
+
return `P${days}D`;
|
|
820
859
|
};
|
|
821
|
-
//#endregion
|
|
822
|
-
//#region src/pages/components/CurrentPlanBaseline.tsx
|
|
823
860
|
/**
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
* falling back to the plan's rate cards only when items aren't present — see
|
|
829
|
-
* {@link getSubscriptionPlanView}.
|
|
861
|
+
* The intended length of a subscription phase: the matching catalog plan
|
|
862
|
+
* phase's authored `duration` when available (the cleanest source), otherwise
|
|
863
|
+
* computed from the phase's own `activeFrom`/`activeTo`. `undefined` for an
|
|
864
|
+
* open-ended (final) phase — the price schedule labels that one "After that".
|
|
830
865
|
*/
|
|
831
|
-
const
|
|
866
|
+
const phaseDuration = (plan, phase) => {
|
|
867
|
+
const catalog = plan.phases?.find((p) => p.key === phase.key)?.duration;
|
|
868
|
+
if (catalog) return catalog;
|
|
869
|
+
if (!phase.activeTo) return void 0;
|
|
870
|
+
return isoDurationBetween(phase.activeFrom, phase.activeTo);
|
|
871
|
+
};
|
|
872
|
+
/**
|
|
873
|
+
* Resolve EVERY phase of a subscription (not just the active one) to a
|
|
874
|
+
* {@link SubscriptionPhaseView}, ordered by `activeFrom` and tagged
|
|
875
|
+
* past/current/future. Generalizes {@link getSubscriptionPlanView}: each phase's
|
|
876
|
+
* price and entitlements come from its own provisioned items (the authoritative
|
|
877
|
+
* source), falling back to the catalog plan phase with the same `key` when a
|
|
878
|
+
* phase (typically a future one) hasn't been provisioned yet. Powers the
|
|
879
|
+
* subscription details page's current + upcoming + previous phase timeline.
|
|
880
|
+
*/
|
|
881
|
+
const getSubscriptionPhaseViews = (subscription, options) => {
|
|
832
882
|
const plan = subscription.plan;
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
units
|
|
856
|
-
|
|
857
|
-
|
|
883
|
+
const currency = subscription.currency ?? plan.currency;
|
|
884
|
+
const billingCadence = subscription.billingCadence ?? plan.billingCadence;
|
|
885
|
+
const now = Date.now();
|
|
886
|
+
return phasesByActiveFrom(subscription).map((phase) => {
|
|
887
|
+
const rateCards = phaseRateCards(plan, phase);
|
|
888
|
+
return {
|
|
889
|
+
id: phase.id,
|
|
890
|
+
name: phase.name,
|
|
891
|
+
status: phaseStatus(phase, now),
|
|
892
|
+
activeFrom: phase.activeFrom,
|
|
893
|
+
activeTo: phase.activeTo,
|
|
894
|
+
priceLabel: formatPlanPrice({
|
|
895
|
+
...plan,
|
|
896
|
+
billingCadence,
|
|
897
|
+
phases: [{
|
|
898
|
+
key: phase.key,
|
|
899
|
+
name: phase.name,
|
|
900
|
+
rateCards
|
|
901
|
+
}]
|
|
902
|
+
}),
|
|
903
|
+
entitlements: categorizeRateCards(rateCards, {
|
|
904
|
+
currency,
|
|
905
|
+
units: options?.units,
|
|
906
|
+
planBillingCadence: billingCadence
|
|
907
|
+
}),
|
|
908
|
+
billingCadence,
|
|
909
|
+
currency
|
|
910
|
+
};
|
|
858
911
|
});
|
|
859
912
|
};
|
|
913
|
+
/**
|
|
914
|
+
* Build a catalog-shaped {@link Plan} from a subscription's *own* current and
|
|
915
|
+
* future phases (past phases dropped), so the plan-change confirmation page can
|
|
916
|
+
* render the current subscription with the exact same pricing-table card
|
|
917
|
+
* ({@link formatPlanPrice} / `getPlanPriceSchedule` / `PlanEntitlements`) used
|
|
918
|
+
* for the new plan. Each phase's rate cards come from its provisioned items
|
|
919
|
+
* (the authoritative source) — the embedded `subscription.plan` snapshot is
|
|
920
|
+
* unreliable here (it can arrive with `phases: []`), so we never read its
|
|
921
|
+
* phases for pricing. Each phase's `duration` is carried so the price schedule
|
|
922
|
+
* labels read "First 3 months" / "After that" exactly like the new plan, rather
|
|
923
|
+
* than falling back to the phase name (see {@link phaseDuration}).
|
|
924
|
+
*/
|
|
925
|
+
const subscriptionToCurrentPlan = (subscription) => {
|
|
926
|
+
const plan = subscription.plan;
|
|
927
|
+
const now = Date.now();
|
|
928
|
+
const phases = phasesByActiveFrom(subscription).filter((p) => !(p.activeTo && new Date(p.activeTo).getTime() < now)).map((phase) => ({
|
|
929
|
+
key: phase.key,
|
|
930
|
+
name: phase.name,
|
|
931
|
+
duration: phaseDuration(plan, phase),
|
|
932
|
+
rateCards: phaseRateCards(plan, phase)
|
|
933
|
+
}));
|
|
934
|
+
return {
|
|
935
|
+
...plan,
|
|
936
|
+
currency: subscription.currency ?? plan.currency,
|
|
937
|
+
billingCadence: subscription.billingCadence ?? plan.billingCadence,
|
|
938
|
+
phases
|
|
939
|
+
};
|
|
940
|
+
};
|
|
860
941
|
//#endregion
|
|
861
942
|
//#region src/pages/SubscriptionChangeConfirmPage.tsx
|
|
862
943
|
const SubscriptionChangeConfirmPage = () => {
|
|
@@ -896,20 +977,27 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
896
977
|
children: /* @__PURE__ */ jsxs("div", {
|
|
897
978
|
className: "space-y-3",
|
|
898
979
|
children: [
|
|
899
|
-
currentSubscription && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(
|
|
900
|
-
|
|
901
|
-
|
|
980
|
+
currentSubscription && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(PlanSummaryCard, {
|
|
981
|
+
plan: subscriptionToCurrentPlan(currentSubscription),
|
|
982
|
+
label: "Current plan",
|
|
983
|
+
descriptionFallback: "",
|
|
984
|
+
taxLabel: "",
|
|
985
|
+
taxInclusive: false,
|
|
986
|
+
units: pricing?.units,
|
|
987
|
+
collapsibleDetails: true
|
|
902
988
|
}), /* @__PURE__ */ jsxs("div", {
|
|
903
989
|
className: "flex items-center justify-center gap-1.5 text-sm text-muted-foreground",
|
|
904
990
|
children: [/* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-4" }), " Changing to"]
|
|
905
991
|
})] }),
|
|
906
992
|
selectedPlan && /* @__PURE__ */ jsx(PlanSummaryCard, {
|
|
907
993
|
plan: selectedPlan,
|
|
908
|
-
|
|
994
|
+
label: "New plan",
|
|
995
|
+
descriptionFallback: "",
|
|
909
996
|
taxAmount,
|
|
910
997
|
taxLabel,
|
|
911
998
|
taxInclusive,
|
|
912
|
-
units: pricing?.units
|
|
999
|
+
units: pricing?.units,
|
|
1000
|
+
collapsibleDetails: true
|
|
913
1001
|
}),
|
|
914
1002
|
/* @__PURE__ */ jsxs("div", {
|
|
915
1003
|
className: "rounded-lg bg-muted/50 p-3 text-sm space-y-1",
|
|
@@ -1431,128 +1519,74 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
|
|
|
1431
1519
|
});
|
|
1432
1520
|
};
|
|
1433
1521
|
//#endregion
|
|
1434
|
-
//#region src/
|
|
1435
|
-
/** Compact, human-readable value for a quota row. */
|
|
1436
|
-
const quotaValueLabel = (q) => {
|
|
1437
|
-
if (q.unitPrice) return q.unitPrice;
|
|
1438
|
-
if (q.tierPrices && q.tierPrices.length > 0) return "Tiered pricing";
|
|
1439
|
-
if (q.isPayg) return "Usage-based";
|
|
1440
|
-
return `${q.limit.toLocaleString("en-US")} / ${q.period}`;
|
|
1441
|
-
};
|
|
1442
|
-
const featureValueLabel = (f) => f.value ?? "Included";
|
|
1443
|
-
const isPlainNumericQuota = (q) => !q.isPayg && !q.unitPrice && (!q.tierPrices || q.tierPrices.length === 0);
|
|
1444
|
-
const sameTierSchedule = (a, b) => (a ?? []).join("\n") === (b ?? []).join("\n");
|
|
1522
|
+
//#region src/pages/components/SubscriptionEntitlements.tsx
|
|
1445
1523
|
/**
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1524
|
+
* Render a subscription's entitlements from a resolved
|
|
1525
|
+
* {@link SubscriptionPlanView}: the active phase's *provisioned* items when
|
|
1526
|
+
* present (the real included quotas + per-unit prices), otherwise the catalog
|
|
1527
|
+
* plan's phases. Centralizes the "items, else fall back to plan phases" branch
|
|
1528
|
+
* shared by the subscription details page and the Switch Plan baseline.
|
|
1529
|
+
*
|
|
1530
|
+
* Renders `null` when there's nothing to show, so callers can wrap it with
|
|
1531
|
+
* {@link hasSubscriptionEntitlements} (for a section header / border) without
|
|
1532
|
+
* leaving an empty container behind.
|
|
1451
1533
|
*/
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
const c = curQuota.get(key);
|
|
1460
|
-
const t = tgtQuota.get(key);
|
|
1461
|
-
if (c && t) {
|
|
1462
|
-
const currentValue = quotaValueLabel(c);
|
|
1463
|
-
const targetValue = quotaValueLabel(t);
|
|
1464
|
-
let change = "same";
|
|
1465
|
-
if (isPlainNumericQuota(c) && isPlainNumericQuota(t) && c.period === t.period) {
|
|
1466
|
-
if (t.limit > c.limit) change = "increase";
|
|
1467
|
-
else if (t.limit < c.limit) change = "decrease";
|
|
1468
|
-
} else if (currentValue !== targetValue || !sameTierSchedule(c.tierPrices, t.tierPrices)) change = "changed";
|
|
1469
|
-
changes.push({
|
|
1470
|
-
key,
|
|
1471
|
-
label: t.name,
|
|
1472
|
-
kind: "quota",
|
|
1473
|
-
change,
|
|
1474
|
-
currentValue,
|
|
1475
|
-
targetValue,
|
|
1476
|
-
tierPrices: t.tierPrices,
|
|
1477
|
-
period: t.period
|
|
1478
|
-
});
|
|
1479
|
-
} else if (t) changes.push({
|
|
1480
|
-
key,
|
|
1481
|
-
label: t.name,
|
|
1482
|
-
kind: "quota",
|
|
1483
|
-
change: "added",
|
|
1484
|
-
targetValue: quotaValueLabel(t),
|
|
1485
|
-
tierPrices: t.tierPrices,
|
|
1486
|
-
period: t.period
|
|
1487
|
-
});
|
|
1488
|
-
else if (c) changes.push({
|
|
1489
|
-
key,
|
|
1490
|
-
label: c.name,
|
|
1491
|
-
kind: "quota",
|
|
1492
|
-
change: "removed",
|
|
1493
|
-
currentValue: quotaValueLabel(c),
|
|
1494
|
-
period: c.period
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
for (const key of new Set([...curFeat.keys(), ...tgtFeat.keys()])) {
|
|
1498
|
-
const c = curFeat.get(key);
|
|
1499
|
-
const t = tgtFeat.get(key);
|
|
1500
|
-
if (c && t) {
|
|
1501
|
-
const currentValue = featureValueLabel(c);
|
|
1502
|
-
const targetValue = featureValueLabel(t);
|
|
1503
|
-
changes.push({
|
|
1504
|
-
key,
|
|
1505
|
-
label: t.name,
|
|
1506
|
-
kind: "feature",
|
|
1507
|
-
change: currentValue === targetValue ? "same" : "changed",
|
|
1508
|
-
currentValue,
|
|
1509
|
-
targetValue
|
|
1510
|
-
});
|
|
1511
|
-
} else if (t) changes.push({
|
|
1512
|
-
key,
|
|
1513
|
-
label: t.name,
|
|
1514
|
-
kind: "feature",
|
|
1515
|
-
change: "added",
|
|
1516
|
-
targetValue: featureValueLabel(t)
|
|
1517
|
-
});
|
|
1518
|
-
else if (c) changes.push({
|
|
1519
|
-
key,
|
|
1520
|
-
label: c.name,
|
|
1521
|
-
kind: "feature",
|
|
1522
|
-
change: "removed",
|
|
1523
|
-
currentValue: featureValueLabel(c)
|
|
1534
|
+
const SubscriptionEntitlements = ({ view, currency, billingCadence, units, itemClassName }) => {
|
|
1535
|
+
if (view.usingItems) {
|
|
1536
|
+
const { quotas, features } = view.entitlements;
|
|
1537
|
+
return /* @__PURE__ */ jsx(EntitlementList, {
|
|
1538
|
+
quotas,
|
|
1539
|
+
features,
|
|
1540
|
+
itemClassName
|
|
1524
1541
|
});
|
|
1525
1542
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
return ch;
|
|
1543
|
+
if (view.fallbackPhases.length === 0) return null;
|
|
1544
|
+
return /* @__PURE__ */ jsx(PlanEntitlements, {
|
|
1545
|
+
phases: view.fallbackPhases,
|
|
1546
|
+
currency,
|
|
1547
|
+
billingCadence,
|
|
1548
|
+
units,
|
|
1549
|
+
itemClassName
|
|
1534
1550
|
});
|
|
1535
1551
|
};
|
|
1536
1552
|
//#endregion
|
|
1537
|
-
//#region src/
|
|
1538
|
-
const durationWithCount = (iso) => {
|
|
1539
|
-
const text = formatDuration(iso);
|
|
1540
|
-
return /^\d/.test(text) ? text : `1 ${text}`;
|
|
1541
|
-
};
|
|
1542
|
-
const steadyStateLabel = (plan) => {
|
|
1543
|
-
const label = formatPlanPrice(plan);
|
|
1544
|
-
if (label.type === "priced") return `${formatPrice(label.amount, plan.currency)} / ${formatDuration(plan.billingCadence)}`;
|
|
1545
|
-
return label.type === "payg" ? "Pay as you go" : "Free";
|
|
1546
|
-
};
|
|
1553
|
+
//#region src/pages/components/CurrentPlanBaseline.tsx
|
|
1547
1554
|
/**
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1555
|
+
* Baseline shown at the top of the Switch Plan modal: the current plan's price
|
|
1556
|
+
* and what it actually includes, so users have a concrete reference to compare
|
|
1557
|
+
* targets against. Both the price and the entitlements come from the
|
|
1558
|
+
* subscription's *provisioned* items (real included quotas + recurring fees),
|
|
1559
|
+
* falling back to the plan's rate cards only when items aren't present — see
|
|
1560
|
+
* {@link getSubscriptionPlanView}.
|
|
1551
1561
|
*/
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
const
|
|
1555
|
-
return
|
|
1562
|
+
const CurrentPlanBaseline = ({ subscription, units }) => {
|
|
1563
|
+
const plan = subscription.plan;
|
|
1564
|
+
const view = getSubscriptionPlanView(subscription, { units });
|
|
1565
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1566
|
+
className: "border rounded-lg p-4",
|
|
1567
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1568
|
+
className: "flex items-start justify-between gap-3 flex-wrap",
|
|
1569
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
1570
|
+
className: "text-xs font-medium text-muted-foreground uppercase tracking-wide",
|
|
1571
|
+
children: "Current Plan"
|
|
1572
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1573
|
+
className: "text-lg font-bold text-foreground",
|
|
1574
|
+
children: plan.name
|
|
1575
|
+
})] }), /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
1576
|
+
label: view.priceLabel,
|
|
1577
|
+
currency: view.currency,
|
|
1578
|
+
billingCadence: view.billingCadence
|
|
1579
|
+
})]
|
|
1580
|
+
}), hasSubscriptionEntitlements(view) && /* @__PURE__ */ jsx("div", {
|
|
1581
|
+
className: "mt-3 pt-3 border-t",
|
|
1582
|
+
children: /* @__PURE__ */ jsx(SubscriptionEntitlements, {
|
|
1583
|
+
view,
|
|
1584
|
+
currency: view.currency,
|
|
1585
|
+
billingCadence: view.billingCadence,
|
|
1586
|
+
units
|
|
1587
|
+
})
|
|
1588
|
+
})]
|
|
1589
|
+
});
|
|
1556
1590
|
};
|
|
1557
1591
|
//#endregion
|
|
1558
1592
|
//#region src/pages/components/PlanChangeCard.tsx
|
|
@@ -1665,18 +1699,28 @@ const ChangeRow = ({ change }) => {
|
|
|
1665
1699
|
const PlanChangeCard = ({ plan, mode, currentEntitlements, isNewerVersion, isSwitching, units, onSwitch }) => {
|
|
1666
1700
|
const isCustom = isCustomPlan(plan);
|
|
1667
1701
|
const priceLabel = formatPlanPrice(plan);
|
|
1668
|
-
const
|
|
1669
|
-
const
|
|
1670
|
-
const
|
|
1671
|
-
|
|
1702
|
+
const schedule = isCustom ? void 0 : getPlanPriceSchedule(plan);
|
|
1703
|
+
const phaseChangeGroups = useMemo(() => {
|
|
1704
|
+
const diff = (target) => {
|
|
1705
|
+
const changes = comparePlanEntitlements(currentEntitlements, target);
|
|
1706
|
+
return [...changes.filter((c) => c.change !== "removed"), ...changes.filter((c) => c.change === "removed")];
|
|
1707
|
+
};
|
|
1708
|
+
const sets = plan.phases.map((phase) => categorizeRateCards(phase.rateCards, {
|
|
1672
1709
|
currency: plan.currency,
|
|
1673
1710
|
units,
|
|
1674
1711
|
planBillingCadence: plan.billingCadence
|
|
1675
|
-
})
|
|
1712
|
+
}));
|
|
1713
|
+
return (plan.phases.length <= 1 || sets.every((set) => sameEntitlementSet(set, sets[0])) ? [{ changes: diff(sets.at(-1) ?? {
|
|
1676
1714
|
quotas: [],
|
|
1677
1715
|
features: []
|
|
1678
|
-
})
|
|
1679
|
-
|
|
1716
|
+
}) }] : plan.phases.flatMap((phase, idx) => {
|
|
1717
|
+
const set = sets[idx];
|
|
1718
|
+
if (set.quotas.length === 0 && set.features.length === 0) return [];
|
|
1719
|
+
return [{
|
|
1720
|
+
phase,
|
|
1721
|
+
changes: diff(set)
|
|
1722
|
+
}];
|
|
1723
|
+
})).filter((group) => group.changes.length > 0);
|
|
1680
1724
|
}, [
|
|
1681
1725
|
plan,
|
|
1682
1726
|
currentEntitlements,
|
|
@@ -1702,7 +1746,7 @@ const PlanChangeCard = ({ plan, mode, currentEntitlements, isNewerVersion, isSwi
|
|
|
1702
1746
|
}), isCustom ? /* @__PURE__ */ jsx("span", {
|
|
1703
1747
|
className: "text-primary font-medium",
|
|
1704
1748
|
children: "Custom"
|
|
1705
|
-
}) : /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
1749
|
+
}) : !schedule && /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
1706
1750
|
label: priceLabel,
|
|
1707
1751
|
currency: plan.currency,
|
|
1708
1752
|
billingCadence: plan.billingCadence
|
|
@@ -1719,13 +1763,22 @@ const PlanChangeCard = ({ plan, mode, currentEntitlements, isNewerVersion, isSwi
|
|
|
1719
1763
|
children: MODE_LABEL[mode]
|
|
1720
1764
|
})]
|
|
1721
1765
|
}),
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1766
|
+
schedule && /* @__PURE__ */ jsx(PlanPriceSchedule, {
|
|
1767
|
+
schedule,
|
|
1768
|
+
currency: plan.currency,
|
|
1769
|
+
billingCadence: plan.billingCadence,
|
|
1770
|
+
className: "mb-2"
|
|
1725
1771
|
}),
|
|
1726
|
-
|
|
1727
|
-
className: "space-y-
|
|
1728
|
-
children:
|
|
1772
|
+
phaseChangeGroups.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1773
|
+
className: "space-y-3",
|
|
1774
|
+
children: phaseChangeGroups.map((group, idx) => /* @__PURE__ */ jsxs("div", {
|
|
1775
|
+
className: "space-y-1.5",
|
|
1776
|
+
children: [group.phase && /* @__PURE__ */ jsx(PlanPhaseHeader, {
|
|
1777
|
+
phase: group.phase,
|
|
1778
|
+
currency: plan.currency,
|
|
1779
|
+
billingCadence: plan.billingCadence
|
|
1780
|
+
}), group.changes.map((change) => /* @__PURE__ */ jsx(ChangeRow, { change }, `${change.kind}:${change.key}`))]
|
|
1781
|
+
}, group.phase?.key ?? String(idx)))
|
|
1729
1782
|
})
|
|
1730
1783
|
]
|
|
1731
1784
|
});
|
|
@@ -2014,6 +2067,75 @@ const ManageSubscription = ({ subscription, planName }) => {
|
|
|
2014
2067
|
] });
|
|
2015
2068
|
};
|
|
2016
2069
|
//#endregion
|
|
2070
|
+
//#region src/pages/components/SubscriptionPhases.tsx
|
|
2071
|
+
const phaseDateLabel = (view) => {
|
|
2072
|
+
if (view.status === "current") return "Current phase";
|
|
2073
|
+
if (view.status === "future") return `Starts ${formatDateTime(view.activeFrom)}`;
|
|
2074
|
+
return view.activeTo ? `${formatDateTime(view.activeFrom)} – ${formatDateTime(view.activeTo)}` : formatDateTime(view.activeFrom);
|
|
2075
|
+
};
|
|
2076
|
+
/**
|
|
2077
|
+
* One phase of a subscription: a header with the phase name, its timing
|
|
2078
|
+
* sub-label, and the phase's own price, followed by that phase's included
|
|
2079
|
+
* quotas/features. The header always renders (even when the phase has no
|
|
2080
|
+
* entitlements) so the price/timing is never hidden.
|
|
2081
|
+
*/
|
|
2082
|
+
const SubscriptionPhaseSection = ({ view }) => {
|
|
2083
|
+
const dateLabel = phaseDateLabel(view);
|
|
2084
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2085
|
+
className: "space-y-2",
|
|
2086
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2087
|
+
className: "flex items-baseline justify-between gap-3",
|
|
2088
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2089
|
+
className: "text-sm font-medium text-card-foreground",
|
|
2090
|
+
children: [view.name, dateLabel && /* @__PURE__ */ jsxs("span", {
|
|
2091
|
+
className: "text-muted-foreground font-normal",
|
|
2092
|
+
children: [
|
|
2093
|
+
" ",
|
|
2094
|
+
"· ",
|
|
2095
|
+
dateLabel
|
|
2096
|
+
]
|
|
2097
|
+
})]
|
|
2098
|
+
}), /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
2099
|
+
label: view.priceLabel,
|
|
2100
|
+
currency: view.currency,
|
|
2101
|
+
billingCadence: view.billingCadence
|
|
2102
|
+
})]
|
|
2103
|
+
}), /* @__PURE__ */ jsx(EntitlementList, {
|
|
2104
|
+
quotas: view.entitlements.quotas,
|
|
2105
|
+
features: view.entitlements.features
|
|
2106
|
+
})]
|
|
2107
|
+
});
|
|
2108
|
+
};
|
|
2109
|
+
/**
|
|
2110
|
+
* The subscription's future phases (each with its start date, price, and
|
|
2111
|
+
* entitlements), shown after the current phase on the details page and on the
|
|
2112
|
+
* Switch Plan baseline. Renders nothing when there are no upcoming phases.
|
|
2113
|
+
*/
|
|
2114
|
+
const UpcomingPhases = ({ subscription, units }) => {
|
|
2115
|
+
const future = getSubscriptionPhaseViews(subscription, { units }).filter((v) => v.status === "future");
|
|
2116
|
+
if (future.length === 0) return null;
|
|
2117
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2118
|
+
className: "space-y-4",
|
|
2119
|
+
children: future.map((view) => /* @__PURE__ */ jsx(SubscriptionPhaseSection, { view }, view.id))
|
|
2120
|
+
});
|
|
2121
|
+
};
|
|
2122
|
+
/**
|
|
2123
|
+
* The subscription's already-ended phases, collapsed by default, each labelled
|
|
2124
|
+
* with the exact dates it ran. Lets users review what they previously paid for
|
|
2125
|
+
* without cluttering the page. Renders nothing when there are no past phases.
|
|
2126
|
+
*/
|
|
2127
|
+
const PreviousPhases = ({ subscription, units }) => {
|
|
2128
|
+
const past = getSubscriptionPhaseViews(subscription, { units }).filter((v) => v.status === "past");
|
|
2129
|
+
if (past.length === 0) return null;
|
|
2130
|
+
return /* @__PURE__ */ jsxs(Collapsible, { children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
2131
|
+
className: "group flex w-full items-center justify-between text-sm font-medium",
|
|
2132
|
+
children: ["Previous phases", /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })]
|
|
2133
|
+
}), /* @__PURE__ */ jsx(CollapsibleContent, {
|
|
2134
|
+
className: "mt-3 space-y-4",
|
|
2135
|
+
children: past.map((view) => /* @__PURE__ */ jsx(SubscriptionPhaseSection, { view }, view.id))
|
|
2136
|
+
})] });
|
|
2137
|
+
};
|
|
2138
|
+
//#endregion
|
|
2017
2139
|
//#region src/pages/subscriptions/SubscriptionPlanDetails.tsx
|
|
2018
2140
|
const detailLabelClassName = "text-sm font-semibold tracking-wide mb-1";
|
|
2019
2141
|
const sectionLabelClassName = "text-base font-semibold tracking-wide mb-3 mt-2";
|
|
@@ -2025,6 +2147,9 @@ const SubscriptionPlanDetails = ({ subscription }) => {
|
|
|
2025
2147
|
const { priceLabel } = view;
|
|
2026
2148
|
const taxLegendSentence = planHasDefaultTaxBehavior(plan) ? subscriptionTaxLegendSentence(plan.defaultTaxConfig?.behavior ?? "") : void 0;
|
|
2027
2149
|
const hasEntitlements = hasSubscriptionEntitlements(view);
|
|
2150
|
+
const phaseViews = getSubscriptionPhaseViews(subscription, { units: pricing?.units });
|
|
2151
|
+
const hasUpcomingPhases = phaseViews.some((p) => p.status === "future");
|
|
2152
|
+
const hasPreviousPhases = phaseViews.some((p) => p.status === "past");
|
|
2028
2153
|
return /* @__PURE__ */ jsxs("div", {
|
|
2029
2154
|
className: "space-y-4",
|
|
2030
2155
|
children: [/* @__PURE__ */ jsx(Heading, {
|
|
@@ -2035,58 +2160,78 @@ const SubscriptionPlanDetails = ({ subscription }) => {
|
|
|
2035
2160
|
children: plan.name
|
|
2036
2161
|
}), plan.description ? /* @__PURE__ */ jsx(CardDescription, { children: plan.description }) : null] }), /* @__PURE__ */ jsxs(CardContent, {
|
|
2037
2162
|
className: "space-y-6",
|
|
2038
|
-
children: [
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
className:
|
|
2082
|
-
children: "
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2163
|
+
children: [
|
|
2164
|
+
/* @__PURE__ */ jsxs("dl", {
|
|
2165
|
+
className: "grid gap-4 sm:grid-cols-2 text-sm",
|
|
2166
|
+
children: [
|
|
2167
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
2168
|
+
className: detailLabelClassName,
|
|
2169
|
+
children: "Subscription ID"
|
|
2170
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
2171
|
+
className: "text-foreground font-mono text-xs break-all",
|
|
2172
|
+
children: subscription.id
|
|
2173
|
+
})] }),
|
|
2174
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
2175
|
+
className: detailLabelClassName,
|
|
2176
|
+
children: "Active since"
|
|
2177
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
2178
|
+
className: "text-foreground",
|
|
2179
|
+
children: formatDateTime(subscription.activeFrom)
|
|
2180
|
+
})] }),
|
|
2181
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
2182
|
+
className: detailLabelClassName,
|
|
2183
|
+
children: "Price"
|
|
2184
|
+
}), /* @__PURE__ */ jsxs("dd", { children: [/* @__PURE__ */ jsx("div", {
|
|
2185
|
+
className: "flex flex-wrap items-baseline gap-1",
|
|
2186
|
+
children: /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
2187
|
+
label: priceLabel,
|
|
2188
|
+
currency: view.currency,
|
|
2189
|
+
billingCadence: view.billingCadence,
|
|
2190
|
+
description: true
|
|
2191
|
+
})
|
|
2192
|
+
}), taxLegendSentence ? /* @__PURE__ */ jsx("p", {
|
|
2193
|
+
className: "text-xs text-muted-foreground mt-1",
|
|
2194
|
+
children: taxLegendSentence
|
|
2195
|
+
}) : null] })] }),
|
|
2196
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
2197
|
+
className: detailLabelClassName,
|
|
2198
|
+
children: "Current period"
|
|
2199
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
2200
|
+
className: "text-foreground",
|
|
2201
|
+
children: subscription.alignment?.currentAlignedBillingPeriod ? formatDateTimeRange(subscription.alignment.currentAlignedBillingPeriod.from, subscription.alignment.currentAlignedBillingPeriod.to) : "—"
|
|
2202
|
+
})] })
|
|
2203
|
+
]
|
|
2204
|
+
}),
|
|
2205
|
+
hasEntitlements ? /* @__PURE__ */ jsxs("div", {
|
|
2206
|
+
className: "space-y-2 pt-2 border-t border-border",
|
|
2207
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
2208
|
+
className: sectionLabelClassName,
|
|
2209
|
+
children: "What's included"
|
|
2210
|
+
}), /* @__PURE__ */ jsx(SubscriptionEntitlements, {
|
|
2211
|
+
view,
|
|
2212
|
+
currency: view.currency,
|
|
2213
|
+
billingCadence: view.billingCadence,
|
|
2214
|
+
units: pricing?.units
|
|
2215
|
+
})]
|
|
2216
|
+
}) : null,
|
|
2217
|
+
hasUpcomingPhases ? /* @__PURE__ */ jsxs("div", {
|
|
2218
|
+
className: "space-y-3 pt-2 border-t border-border",
|
|
2219
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
2220
|
+
className: sectionLabelClassName,
|
|
2221
|
+
children: "Upcoming phases"
|
|
2222
|
+
}), /* @__PURE__ */ jsx(UpcomingPhases, {
|
|
2223
|
+
subscription,
|
|
2224
|
+
units: pricing?.units
|
|
2225
|
+
})]
|
|
2226
|
+
}) : null,
|
|
2227
|
+
hasPreviousPhases ? /* @__PURE__ */ jsx("div", {
|
|
2228
|
+
className: "pt-2 border-t border-border",
|
|
2229
|
+
children: /* @__PURE__ */ jsx(PreviousPhases, {
|
|
2230
|
+
subscription,
|
|
2231
|
+
units: pricing?.units
|
|
2232
|
+
})
|
|
2233
|
+
}) : null
|
|
2234
|
+
]
|
|
2090
2235
|
})] })]
|
|
2091
2236
|
});
|
|
2092
2237
|
};
|