@carlonicora/nextjs-jsonapi 1.32.1 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/{BlockNoteEditor-YEVSJSOI.js → BlockNoteEditor-CUXI6ZTZ.js} +14 -14
  2. package/dist/{BlockNoteEditor-YEVSJSOI.js.map → BlockNoteEditor-CUXI6ZTZ.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-TFL6ZXIJ.mjs → BlockNoteEditor-UTZ7F23J.mjs} +4 -4
  4. package/dist/billing/index.d.mts +6 -3
  5. package/dist/billing/index.d.ts +6 -3
  6. package/dist/billing/index.js +465 -384
  7. package/dist/billing/index.js.map +1 -1
  8. package/dist/billing/index.mjs +114 -33
  9. package/dist/billing/index.mjs.map +1 -1
  10. package/dist/{chunk-NPNKFWV2.js → chunk-2PHWAL6Q.js} +4 -4
  11. package/dist/chunk-2PHWAL6Q.js.map +1 -0
  12. package/dist/{chunk-SLANIL6B.mjs → chunk-53WT73E6.mjs} +56 -64
  13. package/dist/chunk-53WT73E6.mjs.map +1 -0
  14. package/dist/{chunk-YCP2OMFD.mjs → chunk-HWQBSVBT.mjs} +40 -7
  15. package/dist/chunk-HWQBSVBT.mjs.map +1 -0
  16. package/dist/{chunk-HIF7DYR3.js → chunk-RSHCU3TI.js} +553 -561
  17. package/dist/chunk-RSHCU3TI.js.map +1 -0
  18. package/dist/{chunk-KYG2PIRB.js → chunk-TZRAOUAR.js} +118 -85
  19. package/dist/chunk-TZRAOUAR.js.map +1 -0
  20. package/dist/{chunk-IXVNXOZT.mjs → chunk-XLMJPA4N.mjs} +4 -4
  21. package/dist/{chunk-IXVNXOZT.mjs.map → chunk-XLMJPA4N.mjs.map} +1 -1
  22. package/dist/client/index.d.mts +7 -6
  23. package/dist/client/index.d.ts +7 -6
  24. package/dist/client/index.js +4 -4
  25. package/dist/client/index.mjs +3 -3
  26. package/dist/components/index.d.mts +4 -3
  27. package/dist/components/index.d.ts +4 -3
  28. package/dist/components/index.js +4 -4
  29. package/dist/components/index.mjs +3 -3
  30. package/dist/{config-CHwoRDOp.d.ts → config-BbaBV_yk.d.ts} +1 -1
  31. package/dist/{config-DiWyJzk9.d.mts → config-BxwhHdCD.d.mts} +1 -1
  32. package/dist/{content.interface-BSpowEiW.d.mts → content.interface-CWV0q4lZ.d.mts} +1 -1
  33. package/dist/{content.interface-DFQ7mkpL.d.ts → content.interface-CgUu4771.d.ts} +1 -1
  34. package/dist/contexts/index.d.mts +3 -2
  35. package/dist/contexts/index.d.ts +3 -2
  36. package/dist/contexts/index.js +4 -4
  37. package/dist/contexts/index.mjs +3 -3
  38. package/dist/core/index.d.mts +17 -8
  39. package/dist/core/index.d.ts +17 -8
  40. package/dist/core/index.js +6 -2
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/index.mjs +5 -1
  43. package/dist/feature.interface-BxFFOPNq.d.mts +19 -0
  44. package/dist/feature.interface-CIWxo8NP.d.ts +19 -0
  45. package/dist/index.d.mts +10 -9
  46. package/dist/index.d.ts +10 -9
  47. package/dist/index.js +7 -3
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +6 -2
  50. package/dist/{notification.interface-D5MbtfZK.d.mts → notification.interface-DIln2r7X.d.mts} +2 -17
  51. package/dist/{notification.interface-CmKmObIU.d.ts → notification.interface-XARGKJAq.d.ts} +2 -17
  52. package/dist/{s3.service-CoC0k0iu.d.ts → s3.service-DcqkGrKD.d.ts} +12 -3
  53. package/dist/{s3.service-Duh9HW2n.d.mts → s3.service-ag6M_7GO.d.mts} +12 -3
  54. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js +1 -1
  55. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js.map +1 -1
  56. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js +1 -1
  57. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js.map +1 -1
  58. package/dist/server/index.d.mts +4 -3
  59. package/dist/server/index.d.ts +4 -3
  60. package/dist/server/index.js +3 -3
  61. package/dist/server/index.mjs +1 -1
  62. package/dist/{stripe-subscription.interface-BaZUngWe.d.ts → stripe-subscription.interface-Dm__xmvE.d.ts} +3 -0
  63. package/dist/{stripe-subscription.interface-Cm_It1fz.d.mts → stripe-subscription.interface-_VWPY2AA.d.mts} +3 -0
  64. package/dist/{useDataListRetriever-futhx3OP.d.mts → useDataListRetriever-BqJSFBck.d.mts} +1 -0
  65. package/dist/{useDataListRetriever-futhx3OP.d.ts → useDataListRetriever-BqJSFBck.d.ts} +1 -0
  66. package/dist/{useSocket-DUqGoPya.d.mts → useSocket-BILAdmZ0.d.mts} +1 -1
  67. package/dist/{useSocket-QuHa0ZmO.d.ts → useSocket-awibcC9B.d.ts} +1 -1
  68. package/package.json +1 -1
  69. package/scripts/generate-web-module/templates/pages/detail-page.template.ts +1 -1
  70. package/scripts/generate-web-module/templates/pages/list-page.template.ts +1 -1
  71. package/src/components/forms/DatePickerPopover.tsx +17 -15
  72. package/src/components/tables/ContentListTable.tsx +2 -2
  73. package/src/core/abstracts/AbstractService.ts +25 -0
  74. package/src/core/abstracts/ClientAbstractService.ts +10 -0
  75. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +4 -1
  76. package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +1 -1
  77. package/src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx +1 -1
  78. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +85 -1
  79. package/src/features/billing/stripe-price/data/stripe-price.interface.ts +3 -0
  80. package/src/features/billing/stripe-price/data/stripe-price.ts +18 -0
  81. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +5 -2
  82. package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +5 -18
  83. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +1 -1
  84. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +16 -12
  85. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +14 -3
  86. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +14 -9
  87. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +1 -1
  88. package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +2 -2
  89. package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +1 -1
  90. package/src/features/company/components/details/TokenStatusIndicator.tsx +4 -6
  91. package/src/features/company/hooks/useSubscriptionStatus.ts +18 -0
  92. package/src/features/content/hooks/useContentTableStructure.tsx +1 -1
  93. package/src/features/user/contexts/CurrentUserContext.tsx +2 -1
  94. package/src/features/user/hooks/useUserTableStructure.tsx +1 -1
  95. package/src/hooks/useDataListRetriever.ts +13 -0
  96. package/src/login/config.ts +6 -6
  97. package/src/shadcnui/ui/table.tsx +20 -49
  98. package/dist/chunk-HIF7DYR3.js.map +0 -1
  99. package/dist/chunk-KYG2PIRB.js.map +0 -1
  100. package/dist/chunk-NPNKFWV2.js.map +0 -1
  101. package/dist/chunk-SLANIL6B.mjs.map +0 -1
  102. package/dist/chunk-YCP2OMFD.mjs.map +0 -1
  103. /package/dist/{BlockNoteEditor-TFL6ZXIJ.mjs.map → BlockNoteEditor-UTZ7F23J.mjs.map} +0 -0
@@ -19,6 +19,7 @@ import {
19
19
  Input,
20
20
  Label,
21
21
  } from "../../../../../shadcnui";
22
+ import { FeatureInterface, FeatureService } from "../../../../feature";
22
23
  import { StripePriceInterface, StripePriceService } from "../../data";
23
24
 
24
25
  type PriceEditorProps = {
@@ -40,10 +41,25 @@ type PriceFormValues = {
40
41
  description?: string;
41
42
  features: string[];
42
43
  token: string;
44
+ featureIds: string[]; // Platform Feature entity IDs
43
45
  };
44
46
 
45
47
  export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }: PriceEditorProps) {
46
48
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
49
+ const [allFeatures, setAllFeatures] = useState<FeatureInterface[]>([]);
50
+
51
+ // Fetch all platform features on mount
52
+ useEffect(() => {
53
+ const fetchFeatures = async () => {
54
+ try {
55
+ const features = await FeatureService.findMany({});
56
+ setAllFeatures(features);
57
+ } catch (error) {
58
+ console.error("[PriceEditor] Failed to fetch features:", error);
59
+ }
60
+ };
61
+ fetchFeatures();
62
+ }, []);
47
63
 
48
64
  const formSchema = z.object({
49
65
  unitAmount: z.preprocess(
@@ -62,6 +78,7 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
62
78
  description: z.string().optional(),
63
79
  features: z.array(z.string()),
64
80
  token: z.string(),
81
+ featureIds: z.array(z.string()),
65
82
  });
66
83
 
67
84
  const isEditMode = !!price;
@@ -69,6 +86,12 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
69
86
  // Convert cents to dollars for display
70
87
  const defaultUnitAmount = price?.unitAmount ? price.unitAmount / 100 : 0;
71
88
 
89
+ // Get core feature IDs that should always be selected
90
+ const coreFeatureIds = allFeatures.filter((f) => f.isCore).map((f) => f.id);
91
+
92
+ // Combine existing price features with core features (ensure no duplicates)
93
+ const defaultFeatureIds = [...new Set([...(price?.priceFeatures?.map((f) => f.id) ?? []), ...coreFeatureIds])];
94
+
72
95
  const form = useForm<PriceFormValues>({
73
96
  resolver: zodResolver(formSchema) as any,
74
97
  defaultValues: {
@@ -82,12 +105,19 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
82
105
  description: price?.description || "",
83
106
  features: price?.features || [],
84
107
  token: price?.token?.toString() ?? "",
108
+ featureIds: defaultFeatureIds,
85
109
  },
86
110
  });
87
111
 
88
112
  // Reset form when dialog opens to ensure fresh state
89
113
  useEffect(() => {
90
114
  if (open) {
115
+ // Recalculate core feature IDs with current allFeatures
116
+ const currentCoreFeatureIds = allFeatures.filter((f) => f.isCore).map((f) => f.id);
117
+ const resetFeatureIds = [
118
+ ...new Set([...(price?.priceFeatures?.map((f) => f.id) ?? []), ...currentCoreFeatureIds]),
119
+ ];
120
+
91
121
  form.reset({
92
122
  unitAmount: price?.unitAmount ? price.unitAmount / 100 : 0,
93
123
  currency: price?.currency || "usd",
@@ -99,9 +129,10 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
99
129
  description: price?.description || "",
100
130
  features: price?.features || [],
101
131
  token: price?.token?.toString() ?? "",
132
+ featureIds: resetFeatureIds,
102
133
  });
103
134
  }
104
- }, [open, price?.id]);
135
+ }, [open, price?.id, allFeatures]);
105
136
 
106
137
  const watchInterval = form.watch("interval");
107
138
  const isRecurring = watchInterval !== "one_time";
@@ -121,6 +152,8 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
121
152
  description: values.description || undefined,
122
153
  features: values.features.filter((f) => f.trim()) || undefined,
123
154
  token: values.token ? parseInt(values.token, 10) : undefined,
155
+ // Only include featureIds for recurring prices (one-time prices don't support platform features)
156
+ ...(price?.priceType === "recurring" ? { featureIds: values.featureIds } : {}),
124
157
  });
125
158
  } else {
126
159
  // Create new price
@@ -157,6 +190,11 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
157
190
  createInput.token = parseInt(values.token, 10);
158
191
  }
159
192
 
193
+ // Add platform feature IDs only for recurring prices (Neo4j only, not sent to Stripe)
194
+ if (isRecurring && values.featureIds.length > 0) {
195
+ createInput.featureIds = values.featureIds;
196
+ }
197
+
160
198
  await StripePriceService.createPrice(createInput);
161
199
  }
162
200
 
@@ -317,6 +355,52 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
317
355
  </div>
318
356
  </div>
319
357
 
358
+ {/* Platform Features Checkbox List - Only show for recurring prices */}
359
+ {isRecurring && allFeatures.length > 0 && (
360
+ <div className="space-y-2">
361
+ <Label>Platform Features</Label>
362
+ <div className="border rounded-md p-4 space-y-2 max-h-48 overflow-y-auto">
363
+ {allFeatures.map((feature) => {
364
+ const isCore = feature.isCore;
365
+ const isChecked = form.watch("featureIds").includes(feature.id);
366
+
367
+ return (
368
+ <div key={feature.id} className="flex items-center space-x-2">
369
+ <input
370
+ type="checkbox"
371
+ id={`feature-${feature.id}`}
372
+ checked={isChecked}
373
+ disabled={isCore}
374
+ onChange={(e) => {
375
+ const currentIds = form.getValues("featureIds");
376
+ if (e.target.checked) {
377
+ form.setValue("featureIds", [...currentIds, feature.id]);
378
+ } else {
379
+ // Don't allow unchecking core features
380
+ if (!isCore) {
381
+ form.setValue(
382
+ "featureIds",
383
+ currentIds.filter((id) => id !== feature.id),
384
+ );
385
+ }
386
+ }
387
+ }}
388
+ className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-50"
389
+ />
390
+ <label
391
+ htmlFor={`feature-${feature.id}`}
392
+ className={`text-sm ${isCore ? "text-muted-foreground" : ""}`}
393
+ >
394
+ {feature.name}
395
+ {isCore && <span className="ml-2 text-xs text-muted-foreground">(Core - Required)</span>}
396
+ </label>
397
+ </div>
398
+ );
399
+ })}
400
+ </div>
401
+ </div>
402
+ )}
403
+
320
404
  <FormCheckbox form={form} id="active" name="Active" />
321
405
 
322
406
  <CommonEditorButtons isEdit={isEditMode} form={form} disabled={isSubmitting} setOpen={onOpenChange} />
@@ -1,5 +1,6 @@
1
1
  import { ApiDataInterface } from "../../../../core";
2
2
  import { StripeProductInterface } from "../../stripe-product";
3
+ import { FeatureInterface } from "../../../feature";
3
4
 
4
5
  // ============================================================================
5
6
  // Stripe Price Interfaces
@@ -20,6 +21,7 @@ export interface StripePriceInterface extends ApiDataInterface {
20
21
  get description(): string | undefined;
21
22
  get features(): string[] | undefined;
22
23
  get token(): number | undefined;
24
+ get priceFeatures(): FeatureInterface[]; // Platform Feature entities linked to this price
23
25
  }
24
26
 
25
27
  export interface PriceRecurring {
@@ -47,4 +49,5 @@ export type StripePriceInput = {
47
49
  description?: string;
48
50
  features?: string[];
49
51
  token?: number;
52
+ featureIds?: string[]; // Feature entity IDs to link (Neo4j only, NOT sent to Stripe)
50
53
  };
@@ -1,5 +1,6 @@
1
1
  import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../../core";
2
2
  import { StripeProductInterface } from "../../stripe-product";
3
+ import { FeatureInterface } from "../../../feature";
3
4
  import { PriceRecurring, StripePriceInput, StripePriceInterface } from "./stripe-price.interface";
4
5
 
5
6
  export class StripePrice extends AbstractApiData implements StripePriceInterface {
@@ -17,6 +18,7 @@ export class StripePrice extends AbstractApiData implements StripePriceInterface
17
18
  private _description?: string;
18
19
  private _features?: string[];
19
20
  private _token?: number;
21
+ private _priceFeatures: FeatureInterface[] = []; // Platform Feature entities
20
22
 
21
23
  get stripePriceId(): string {
22
24
  if (!this._stripePriceId) throw new Error("stripePriceId is not defined");
@@ -78,6 +80,10 @@ export class StripePrice extends AbstractApiData implements StripePriceInterface
78
80
  return this._token;
79
81
  }
80
82
 
83
+ get priceFeatures(): FeatureInterface[] {
84
+ return this._priceFeatures;
85
+ }
86
+
81
87
  rehydrate(data: JsonApiHydratedDataInterface): this {
82
88
  super.rehydrate(data);
83
89
 
@@ -118,6 +124,9 @@ export class StripePrice extends AbstractApiData implements StripePriceInterface
118
124
  // Hydrate product relationship
119
125
  this._product = this._readIncluded(data, "product", Modules.StripeProduct) as StripeProductInterface;
120
126
 
127
+ // Hydrate feature relationship (JSON:API key is "features")
128
+ this._priceFeatures = this._readIncluded(data, "features", Modules.Feature) as FeatureInterface[];
129
+
121
130
  return this;
122
131
  }
123
132
 
@@ -161,6 +170,15 @@ export class StripePrice extends AbstractApiData implements StripePriceInterface
161
170
  response.data.attributes.token = data.token;
162
171
  }
163
172
 
173
+ // Convert featureIds to JSON:API relationships format
174
+ if (data.featureIds && data.featureIds.length > 0) {
175
+ response.data.relationships = response.data.relationships || {};
176
+ response.data.relationships.features = data.featureIds.map((id) => ({
177
+ type: Modules.Feature.name,
178
+ id,
179
+ }));
180
+ }
181
+
164
182
  return response;
165
183
  }
166
184
  }
@@ -9,9 +9,10 @@ import { SubscriptionsList } from "../lists";
9
9
 
10
10
  type SubscriptionsContainerProps = {
11
11
  onOpenWizard?: (subscription?: StripeSubscriptionInterface) => void;
12
+ hasActiveRecurringSubscription?: boolean;
12
13
  };
13
14
 
14
- export function SubscriptionsContainer({ onOpenWizard }: SubscriptionsContainerProps) {
15
+ export function SubscriptionsContainer({ onOpenWizard, hasActiveRecurringSubscription }: SubscriptionsContainerProps) {
15
16
  const [subscriptions, setSubscriptions] = useState<StripeSubscriptionInterface[]>([]);
16
17
  const [loading, setLoading] = useState<boolean>(true);
17
18
 
@@ -57,7 +58,9 @@ export function SubscriptionsContainer({ onOpenWizard }: SubscriptionsContainerP
57
58
  <h1 className="text-3xl font-bold">Subscriptions</h1>
58
59
  </div>
59
60
  {subscriptions.length > 0 && (
60
- <Button onClick={() => onOpenWizard?.()}>Subscribe to a Plan</Button>
61
+ <Button onClick={() => onOpenWizard?.()}>
62
+ {hasActiveRecurringSubscription ? "Purchase Add-ons" : "Subscribe to a Plan"}
63
+ </Button>
61
64
  )}
62
65
  </div>
63
66
 
@@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
4
4
  import { useState } from "react";
5
5
  import { SubmitHandler, useForm } from "react-hook-form";
6
6
  import { z } from "zod";
7
- import { FormCheckbox, FormTextarea } from "../../../../../components";
7
+ import { FormTextarea } from "../../../../../components";
8
8
  import {
9
9
  Button,
10
10
  Dialog,
@@ -25,7 +25,6 @@ type CancelSubscriptionDialogProps = {
25
25
  };
26
26
 
27
27
  const formSchema = z.object({
28
- cancelImmediately: z.boolean(),
29
28
  reason: z.string().optional(),
30
29
  });
31
30
 
@@ -40,20 +39,17 @@ export function CancelSubscriptionDialog({
40
39
  const form = useForm<z.infer<typeof formSchema>>({
41
40
  resolver: zodResolver(formSchema),
42
41
  defaultValues: {
43
- cancelImmediately: false,
44
42
  reason: "",
45
43
  },
46
44
  });
47
45
 
48
- const cancelImmediately = form.watch("cancelImmediately");
49
-
50
46
  const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (values) => {
51
47
  setIsSubmitting(true);
52
48
 
53
49
  try {
54
50
  await StripeSubscriptionService.cancelSubscription({
55
51
  id: subscription.id,
56
- cancelImmediately: values.cancelImmediately,
52
+ cancelImmediately: false,
57
53
  });
58
54
 
59
55
  onSuccess();
@@ -79,18 +75,9 @@ export function CancelSubscriptionDialog({
79
75
 
80
76
  <Form {...form}>
81
77
  <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
82
- <FormCheckbox form={form} id="cancelImmediately" name="Cancel Immediately" />
83
-
84
- {cancelImmediately ? (
85
- <div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800">
86
- Your subscription will be canceled immediately and you will lose access right away.
87
- </div>
88
- ) : (
89
- <div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm text-blue-800">
90
- Your subscription will remain active until {periodEndDate}. You can continue using the service until
91
- then.
92
- </div>
93
- )}
78
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm text-blue-800">
79
+ Your subscription will remain active until {periodEndDate}. You can continue using the service until then.
80
+ </div>
94
81
 
95
82
  <FormTextarea
96
83
  form={form}
@@ -53,7 +53,7 @@ export function SubscriptionsList({ subscriptions, onSubscriptionsChange, onChan
53
53
 
54
54
  return (
55
55
  <>
56
- <div className="border rounded-lg overflow-hidden">
56
+ <div className="border rounded-lg overflow-clip">
57
57
  <Table>
58
58
  <TableHeader className="bg-muted">
59
59
  <TableRow>
@@ -15,6 +15,7 @@ export type ProductPricingListProps = {
15
15
  loading?: boolean;
16
16
  onSelectPrice: (price: StripePriceInterface) => void;
17
17
  hideRecurringPrices?: boolean;
18
+ hideOneTimePrices?: boolean;
18
19
  };
19
20
 
20
21
  function isRecurringProduct(prices: StripePriceInterface[]): boolean {
@@ -49,6 +50,7 @@ export function ProductPricingList({
49
50
  loading = false,
50
51
  onSelectPrice,
51
52
  hideRecurringPrices = false,
53
+ hideOneTimePrices = false,
52
54
  }: ProductPricingListProps) {
53
55
  if (loading) {
54
56
  return <ProductPricingListSkeleton />;
@@ -66,21 +68,23 @@ export function ProductPricingList({
66
68
  return 0;
67
69
  });
68
70
 
69
- // Filter products based on hideRecurringPrices
70
- const filteredProducts = hideRecurringPrices
71
- ? sortedProducts
72
- .map((product) => ({
73
- ...product,
74
- stripePrices: (product.stripePrices || []).filter((price) => price.priceType !== "recurring"),
75
- }))
76
- .filter((product) => product.stripePrices.length > 0)
77
- : sortedProducts;
78
-
79
71
  return (
80
72
  <div className="space-y-6">
81
- {filteredProducts.map((product) => {
73
+ {sortedProducts.map((product) => {
82
74
  const allPrices = product.stripePrices || [];
83
- const filteredPrices = getFilteredPrices(allPrices, selectedInterval);
75
+
76
+ // Filter prices based on hideRecurringPrices and hideOneTimePrices flags
77
+ let pricesToFilter = allPrices;
78
+
79
+ if (hideRecurringPrices) {
80
+ pricesToFilter = pricesToFilter.filter((price) => price.priceType !== "recurring");
81
+ }
82
+
83
+ if (hideOneTimePrices) {
84
+ pricesToFilter = pricesToFilter.filter((price) => price.priceType !== "one_time");
85
+ }
86
+
87
+ const filteredPrices = getFilteredPrices(pricesToFilter, selectedInterval);
84
88
 
85
89
  if (filteredPrices.length === 0) {
86
90
  return null;
@@ -59,10 +59,20 @@ export function SubscriptionWizard({
59
59
  }
60
60
  }, [open, actions.reset]);
61
61
 
62
- const dialogTitle = subscription ? "Change Subscription Plan" : "Subscribe to a Plan";
62
+ const isPurchasingAddons = hasActiveRecurringSubscription && !subscription;
63
+ const isChangePlanMode = !!subscription;
64
+
65
+ const dialogTitle = subscription
66
+ ? "Change Subscription Plan"
67
+ : isPurchasingAddons
68
+ ? "Purchase Add-ons"
69
+ : "Subscribe to a Plan";
70
+
63
71
  const dialogDescription = subscription
64
72
  ? "Select a new plan for your subscription"
65
- : "Choose a subscription plan to get started";
73
+ : isPurchasingAddons
74
+ ? "Select one-time products to purchase"
75
+ : "Choose a subscription plan to get started";
66
76
 
67
77
  return (
68
78
  <Dialog open={open} onOpenChange={onOpenChange}>
@@ -79,7 +89,8 @@ export function SubscriptionWizard({
79
89
  selectedPrice={state.selectedPrice}
80
90
  selectedInterval={state.selectedInterval}
81
91
  currentPriceId={subscription?.price?.id}
82
- hideRecurringPrices={hasActiveRecurringSubscription && !subscription}
92
+ hideRecurringPrices={isPurchasingAddons}
93
+ hideOneTimePrices={isChangePlanMode}
83
94
  onSelectPrice={actions.selectPrice}
84
95
  onIntervalChange={actions.setInterval}
85
96
  onNext={actions.goToReview}
@@ -12,6 +12,7 @@ type WizardStepPlanSelectionProps = {
12
12
  selectedInterval: BillingInterval;
13
13
  currentPriceId?: string;
14
14
  hideRecurringPrices: boolean;
15
+ hideOneTimePrices: boolean;
15
16
  onSelectPrice: (price: StripePriceInterface) => void;
16
17
  onIntervalChange: (interval: BillingInterval) => void;
17
18
  onNext: () => void;
@@ -23,6 +24,7 @@ export function WizardStepPlanSelection({
23
24
  selectedInterval,
24
25
  currentPriceId,
25
26
  hideRecurringPrices,
27
+ hideOneTimePrices,
26
28
  onSelectPrice,
27
29
  onIntervalChange,
28
30
  onNext,
@@ -71,15 +73,17 @@ export function WizardStepPlanSelection({
71
73
 
72
74
  return (
73
75
  <div className="space-y-6">
74
- {/* Interval Toggle */}
75
- <div className="flex justify-center">
76
- <IntervalToggle
77
- value={selectedInterval}
78
- onChange={onIntervalChange}
79
- hasMonthly={hasMonthly}
80
- hasYearly={hasYearly}
81
- />
82
- </div>
76
+ {/* Interval Toggle - only show for recurring products */}
77
+ {!hideRecurringPrices && (
78
+ <div className="flex justify-center">
79
+ <IntervalToggle
80
+ value={selectedInterval}
81
+ onChange={onIntervalChange}
82
+ hasMonthly={hasMonthly}
83
+ hasYearly={hasYearly}
84
+ />
85
+ </div>
86
+ )}
83
87
 
84
88
  {/* Product Pricing List */}
85
89
  <ProductPricingList
@@ -89,6 +93,7 @@ export function WizardStepPlanSelection({
89
93
  selectedPriceId={selectedPrice?.id}
90
94
  loading={loading}
91
95
  hideRecurringPrices={hideRecurringPrices}
96
+ hideOneTimePrices={hideOneTimePrices}
92
97
  onSelectPrice={handleSelectPrice}
93
98
  />
94
99
 
@@ -77,7 +77,7 @@ export function WizardStepReview({
77
77
  <div className="flex justify-between text-sm">
78
78
  <span className="text-blue-600">Amount due now:</span>
79
79
  <span className="font-medium text-blue-800">
80
- {formatCurrency(prorationPreview.amountDue, prorationPreview.currency)}
80
+ {formatCurrency(prorationPreview.immediateCharge, prorationPreview.currency)}
81
81
  </span>
82
82
  </div>
83
83
  </div>
@@ -89,7 +89,7 @@ export class StripeSubscriptionService extends AbstractService {
89
89
 
90
90
  return this.callApi<StripeSubscriptionInterface>({
91
91
  type: Modules.StripeSubscription,
92
- method: HttpMethod.PUT,
92
+ method: HttpMethod.POST,
93
93
  endpoint: endpoint.generate(),
94
94
  input: params,
95
95
  });
@@ -109,7 +109,7 @@ export class StripeSubscriptionService extends AbstractService {
109
109
  childEndpoint: "proration-preview",
110
110
  });
111
111
 
112
- endpoint.addAdditionalParam("newPriceId", params.newPriceId);
112
+ endpoint.addAdditionalParam("priceId", params.newPriceId);
113
113
  if (params.quantity) {
114
114
  endpoint.addAdditionalParam("quantity", params.quantity.toString());
115
115
  }
@@ -40,7 +40,7 @@ export function UsageHistoryTable({ usageRecords }: UsageHistoryTableProps) {
40
40
  return (
41
41
  <div className="flex w-full flex-col gap-y-4">
42
42
  <h2 className="text-xl font-semibold">Usage History</h2>
43
- <div className="overflow-hidden rounded-lg border">
43
+ <div className="overflow-clip rounded-lg border">
44
44
  <Table>
45
45
  <TableHeader className="bg-muted">
46
46
  <TableRow>
@@ -63,22 +63,20 @@ export function TokenStatusIndicator({ className, size = "md", showExtraPages =
63
63
  const getBatteryIcon = () => {
64
64
  if (percentage > 75) {
65
65
  return <BatteryFull className={cn(iconSize, "text-green-500")} />;
66
+ } else if (percentage > 50) {
67
+ return <BatteryMedium className={cn(iconSize, "text-green-500")} />;
66
68
  } else if (percentage >= 25) {
67
- return <BatteryMedium className={cn(iconSize, "text-yellow-500")} />;
68
- } else if (percentage >= 5) {
69
- return <BatteryLow className={cn(iconSize, "text-orange-500")} />;
69
+ return <BatteryLow className={cn(iconSize, "text-yellow-500")} />;
70
70
  } else {
71
71
  return <Battery className={cn(iconSize, "text-destructive")} />;
72
72
  }
73
73
  };
74
74
 
75
75
  const getStatusColor = () => {
76
- if (percentage > 75) {
76
+ if (percentage > 50) {
77
77
  return "text-green-500";
78
78
  } else if (percentage >= 25) {
79
79
  return "text-yellow-500";
80
- } else if (percentage >= 5) {
81
- return "text-orange-500";
82
80
  } else {
83
81
  return "text-destructive";
84
82
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useMemo } from "react";
4
4
  import { useCurrentUserContext } from "../../user/contexts/CurrentUserContext";
5
+ import { getRoleId, isRolesConfigured } from "../../../roles";
5
6
 
6
7
  export interface TrialSubscriptionStatus {
7
8
  status: "loading" | "trial" | "active" | "expired";
@@ -14,6 +15,12 @@ export interface TrialSubscriptionStatus {
14
15
  const TRIAL_DAYS = 14;
15
16
  const GRACE_DAYS = 3;
16
17
 
18
+ const isAdministrator = (currentUser: any): boolean => {
19
+ if (!currentUser || !isRolesConfigured()) return false;
20
+ const adminRoleId = getRoleId().Administrator;
21
+ return !!currentUser.roles?.some((role: any) => role.id === adminRoleId);
22
+ };
23
+
17
24
  export function useSubscriptionStatus(): TrialSubscriptionStatus {
18
25
  const { company, currentUser } = useCurrentUserContext();
19
26
 
@@ -29,6 +36,17 @@ export function useSubscriptionStatus(): TrialSubscriptionStatus {
29
36
  };
30
37
  }
31
38
 
39
+ // Administrator users are never blocked by trial
40
+ if (isAdministrator(currentUser)) {
41
+ return {
42
+ status: "active",
43
+ trialEndsAt: null,
44
+ daysRemaining: 0,
45
+ isGracePeriod: false,
46
+ isBlocked: false,
47
+ };
48
+ }
49
+
32
50
  // No company after loading = blocked
33
51
  if (!company) {
34
52
  return {
@@ -73,7 +73,7 @@ export const useContentTableStructure = <U extends string = ContentFields>(
73
73
  const response = `${content.relevance.toFixed(0)}%`;
74
74
 
75
75
  return (
76
- <div className="relative flex h-5 w-20 items-center justify-center overflow-hidden rounded border text-center">
76
+ <div className="relative flex h-5 w-20 items-center justify-center overflow-clip rounded border text-center">
77
77
  <div
78
78
  className={`bg-accent absolute top-0 left-0 h-full opacity-${Math.round(content.relevance)}`}
79
79
  style={{ width: `${content.relevance}%` }}
@@ -5,6 +5,7 @@ import { useAtom } from "jotai";
5
5
  import { atomWithStorage } from "jotai/utils";
6
6
  import { usePathname } from "next/navigation";
7
7
  import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
8
+ import { useSocketContext } from "../../../contexts/SocketContext";
8
9
  import { Modules, rehydrate } from "../../../core";
9
10
  import { Action, checkPermissions, ModuleWithPermissions } from "../../../permissions";
10
11
  import { getRoleId } from "../../../roles";
@@ -12,7 +13,6 @@ import { CompanyInterface } from "../../company/data/company.interface";
12
13
  import { FeatureInterface } from "../../feature";
13
14
  import { RoleInterface } from "../../role";
14
15
  import { UserInterface, UserService } from "../data";
15
- import { useSocketContext } from "../../../contexts/SocketContext";
16
16
 
17
17
  export interface CurrentUserContextType<T extends UserInterface = UserInterface> {
18
18
  currentUser: T | null;
@@ -143,6 +143,7 @@ export const CurrentUserProvider = ({ children }: { children: React.ReactNode })
143
143
  const fullUser = await UserService.findFullUser();
144
144
  if (fullUser) {
145
145
  setDehydratedUser(fullUser.dehydrate() as any);
146
+ setUser(fullUser);
146
147
  }
147
148
  } catch (error) {
148
149
  console.error("Failed to refresh user data:", error);
@@ -80,7 +80,7 @@ export const useUserTableStructure: UseTableStructureHook<UserInterface, UserFie
80
80
  const response = `${user.relevance.toFixed(0)}%`;
81
81
 
82
82
  return (
83
- <div className="relative flex h-5 w-20 items-center justify-center overflow-hidden rounded border text-center">
83
+ <div className="relative flex h-5 w-20 items-center justify-center overflow-clip rounded border text-center">
84
84
  <div
85
85
  className={`bg-accent absolute top-0 left-0 h-full opacity-${Math.round(user.relevance)}`}
86
86
  style={{ width: `${user.relevance}%` }}