@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.
- package/dist/{BlockNoteEditor-YEVSJSOI.js → BlockNoteEditor-CUXI6ZTZ.js} +14 -14
- package/dist/{BlockNoteEditor-YEVSJSOI.js.map → BlockNoteEditor-CUXI6ZTZ.js.map} +1 -1
- package/dist/{BlockNoteEditor-TFL6ZXIJ.mjs → BlockNoteEditor-UTZ7F23J.mjs} +4 -4
- package/dist/billing/index.d.mts +6 -3
- package/dist/billing/index.d.ts +6 -3
- package/dist/billing/index.js +465 -384
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +114 -33
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-NPNKFWV2.js → chunk-2PHWAL6Q.js} +4 -4
- package/dist/chunk-2PHWAL6Q.js.map +1 -0
- package/dist/{chunk-SLANIL6B.mjs → chunk-53WT73E6.mjs} +56 -64
- package/dist/chunk-53WT73E6.mjs.map +1 -0
- package/dist/{chunk-YCP2OMFD.mjs → chunk-HWQBSVBT.mjs} +40 -7
- package/dist/chunk-HWQBSVBT.mjs.map +1 -0
- package/dist/{chunk-HIF7DYR3.js → chunk-RSHCU3TI.js} +553 -561
- package/dist/chunk-RSHCU3TI.js.map +1 -0
- package/dist/{chunk-KYG2PIRB.js → chunk-TZRAOUAR.js} +118 -85
- package/dist/chunk-TZRAOUAR.js.map +1 -0
- package/dist/{chunk-IXVNXOZT.mjs → chunk-XLMJPA4N.mjs} +4 -4
- package/dist/{chunk-IXVNXOZT.mjs.map → chunk-XLMJPA4N.mjs.map} +1 -1
- package/dist/client/index.d.mts +7 -6
- package/dist/client/index.d.ts +7 -6
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +4 -3
- package/dist/components/index.d.ts +4 -3
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/{config-CHwoRDOp.d.ts → config-BbaBV_yk.d.ts} +1 -1
- package/dist/{config-DiWyJzk9.d.mts → config-BxwhHdCD.d.mts} +1 -1
- package/dist/{content.interface-BSpowEiW.d.mts → content.interface-CWV0q4lZ.d.mts} +1 -1
- package/dist/{content.interface-DFQ7mkpL.d.ts → content.interface-CgUu4771.d.ts} +1 -1
- package/dist/contexts/index.d.mts +3 -2
- package/dist/contexts/index.d.ts +3 -2
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +17 -8
- package/dist/core/index.d.ts +17 -8
- package/dist/core/index.js +6 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +5 -1
- package/dist/feature.interface-BxFFOPNq.d.mts +19 -0
- package/dist/feature.interface-CIWxo8NP.d.ts +19 -0
- package/dist/index.d.mts +10 -9
- package/dist/index.d.ts +10 -9
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -2
- package/dist/{notification.interface-D5MbtfZK.d.mts → notification.interface-DIln2r7X.d.mts} +2 -17
- package/dist/{notification.interface-CmKmObIU.d.ts → notification.interface-XARGKJAq.d.ts} +2 -17
- package/dist/{s3.service-CoC0k0iu.d.ts → s3.service-DcqkGrKD.d.ts} +12 -3
- package/dist/{s3.service-Duh9HW2n.d.mts → s3.service-ag6M_7GO.d.mts} +12 -3
- package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js +1 -1
- package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/pages/list-page.template.js +1 -1
- package/dist/scripts/generate-web-module/templates/pages/list-page.template.js.map +1 -1
- package/dist/server/index.d.mts +4 -3
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/{stripe-subscription.interface-BaZUngWe.d.ts → stripe-subscription.interface-Dm__xmvE.d.ts} +3 -0
- package/dist/{stripe-subscription.interface-Cm_It1fz.d.mts → stripe-subscription.interface-_VWPY2AA.d.mts} +3 -0
- package/dist/{useDataListRetriever-futhx3OP.d.mts → useDataListRetriever-BqJSFBck.d.mts} +1 -0
- package/dist/{useDataListRetriever-futhx3OP.d.ts → useDataListRetriever-BqJSFBck.d.ts} +1 -0
- package/dist/{useSocket-DUqGoPya.d.mts → useSocket-BILAdmZ0.d.mts} +1 -1
- package/dist/{useSocket-QuHa0ZmO.d.ts → useSocket-awibcC9B.d.ts} +1 -1
- package/package.json +1 -1
- package/scripts/generate-web-module/templates/pages/detail-page.template.ts +1 -1
- package/scripts/generate-web-module/templates/pages/list-page.template.ts +1 -1
- package/src/components/forms/DatePickerPopover.tsx +17 -15
- package/src/components/tables/ContentListTable.tsx +2 -2
- package/src/core/abstracts/AbstractService.ts +25 -0
- package/src/core/abstracts/ClientAbstractService.ts +10 -0
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +4 -1
- package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +1 -1
- package/src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx +1 -1
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +85 -1
- package/src/features/billing/stripe-price/data/stripe-price.interface.ts +3 -0
- package/src/features/billing/stripe-price/data/stripe-price.ts +18 -0
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +5 -2
- package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +5 -18
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +1 -1
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +16 -12
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +14 -3
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +14 -9
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +1 -1
- package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +2 -2
- package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +1 -1
- package/src/features/company/components/details/TokenStatusIndicator.tsx +4 -6
- package/src/features/company/hooks/useSubscriptionStatus.ts +18 -0
- package/src/features/content/hooks/useContentTableStructure.tsx +1 -1
- package/src/features/user/contexts/CurrentUserContext.tsx +2 -1
- package/src/features/user/hooks/useUserTableStructure.tsx +1 -1
- package/src/hooks/useDataListRetriever.ts +13 -0
- package/src/login/config.ts +6 -6
- package/src/shadcnui/ui/table.tsx +20 -49
- package/dist/chunk-HIF7DYR3.js.map +0 -1
- package/dist/chunk-KYG2PIRB.js.map +0 -1
- package/dist/chunk-NPNKFWV2.js.map +0 -1
- package/dist/chunk-SLANIL6B.mjs.map +0 -1
- package/dist/chunk-YCP2OMFD.mjs.map +0 -1
- /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
|
}
|
package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx
CHANGED
|
@@ -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?.()}>
|
|
61
|
+
<Button onClick={() => onOpenWizard?.()}>
|
|
62
|
+
{hasActiveRecurringSubscription ? "Purchase Add-ons" : "Subscribe to a Plan"}
|
|
63
|
+
</Button>
|
|
61
64
|
)}
|
|
62
65
|
</div>
|
|
63
66
|
|
package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
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-
|
|
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
|
-
{
|
|
73
|
+
{sortedProducts.map((product) => {
|
|
82
74
|
const allPrices = product.stripePrices || [];
|
|
83
|
-
|
|
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
|
|
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
|
-
:
|
|
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={
|
|
92
|
+
hideRecurringPrices={isPurchasingAddons}
|
|
93
|
+
hideOneTimePrices={isChangePlanMode}
|
|
83
94
|
onSelectPrice={actions.selectPrice}
|
|
84
95
|
onIntervalChange={actions.setInterval}
|
|
85
96
|
onNext={actions.goToReview}
|
package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx
CHANGED
|
@@ -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
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
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.
|
|
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("
|
|
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-
|
|
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 <
|
|
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 >
|
|
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-
|
|
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-
|
|
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}%` }}
|