@carlonicora/nextjs-jsonapi 1.32.2 → 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-76UO4RKT.js → BlockNoteEditor-CUXI6ZTZ.js} +13 -13
- package/dist/{BlockNoteEditor-76UO4RKT.js.map → BlockNoteEditor-CUXI6ZTZ.js.map} +1 -1
- package/dist/{BlockNoteEditor-EWOPRRJN.mjs → BlockNoteEditor-UTZ7F23J.mjs} +3 -3
- package/dist/billing/index.d.mts +6 -3
- package/dist/billing/index.d.ts +6 -3
- package/dist/billing/index.js +461 -380
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +113 -32
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-CCCAOXA2.mjs → chunk-53WT73E6.mjs} +55 -63
- 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-SS6ZCTYH.js → chunk-RSHCU3TI.js} +508 -516
- 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/client/index.d.mts +7 -6
- package/dist/client/index.d.ts +7 -6
- package/dist/client/index.js +3 -3
- package/dist/client/index.mjs +2 -2
- package/dist/components/index.d.mts +4 -3
- package/dist/components/index.d.ts +4 -3
- package/dist/components/index.js +3 -3
- package/dist/components/index.mjs +2 -2
- 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 +3 -3
- package/dist/contexts/index.mjs +2 -2
- 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 +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -1
- 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/shadcnui/ui/table.tsx +20 -49
- package/dist/chunk-CCCAOXA2.mjs.map +0 -1
- package/dist/chunk-KYG2PIRB.js.map +0 -1
- package/dist/chunk-SS6ZCTYH.js.map +0 -1
- package/dist/chunk-YCP2OMFD.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-EWOPRRJN.mjs.map → BlockNoteEditor-UTZ7F23J.mjs.map} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { A as ApiDataInterface } from './ApiDataInterface-DPP8s46n.js';
|
|
2
|
+
import { F as FeatureInterface } from './feature.interface-CIWxo8NP.js';
|
|
2
3
|
|
|
3
4
|
interface StripeUsageInterface extends ApiDataInterface {
|
|
4
5
|
get subscriptionId(): string;
|
|
@@ -151,6 +152,7 @@ interface StripePriceInterface extends ApiDataInterface {
|
|
|
151
152
|
get description(): string | undefined;
|
|
152
153
|
get features(): string[] | undefined;
|
|
153
154
|
get token(): number | undefined;
|
|
155
|
+
get priceFeatures(): FeatureInterface[];
|
|
154
156
|
}
|
|
155
157
|
interface PriceRecurring {
|
|
156
158
|
interval: "day" | "week" | "month" | "year";
|
|
@@ -172,6 +174,7 @@ type StripePriceInput = {
|
|
|
172
174
|
description?: string;
|
|
173
175
|
features?: string[];
|
|
174
176
|
token?: number;
|
|
177
|
+
featureIds?: string[];
|
|
175
178
|
};
|
|
176
179
|
|
|
177
180
|
declare enum SubscriptionStatus {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { A as ApiDataInterface } from './ApiDataInterface-DPP8s46n.mjs';
|
|
2
|
+
import { F as FeatureInterface } from './feature.interface-BxFFOPNq.mjs';
|
|
2
3
|
|
|
3
4
|
interface StripeUsageInterface extends ApiDataInterface {
|
|
4
5
|
get subscriptionId(): string;
|
|
@@ -151,6 +152,7 @@ interface StripePriceInterface extends ApiDataInterface {
|
|
|
151
152
|
get description(): string | undefined;
|
|
152
153
|
get features(): string[] | undefined;
|
|
153
154
|
get token(): number | undefined;
|
|
155
|
+
get priceFeatures(): FeatureInterface[];
|
|
154
156
|
}
|
|
155
157
|
interface PriceRecurring {
|
|
156
158
|
interval: "day" | "week" | "month" | "year";
|
|
@@ -172,6 +174,7 @@ type StripePriceInput = {
|
|
|
172
174
|
description?: string;
|
|
173
175
|
features?: string[];
|
|
174
176
|
token?: number;
|
|
177
|
+
featureIds?: string[];
|
|
175
178
|
};
|
|
176
179
|
|
|
177
180
|
declare enum SubscriptionStatus {
|
|
@@ -8,6 +8,7 @@ type DataListRetriever<T> = {
|
|
|
8
8
|
setReady: (state: boolean) => void;
|
|
9
9
|
isLoaded: boolean;
|
|
10
10
|
data: T[] | undefined;
|
|
11
|
+
total?: number;
|
|
11
12
|
next?: (onlyNewRecords?: boolean) => Promise<void>;
|
|
12
13
|
previous?: (onlyNewRecords?: boolean) => Promise<void>;
|
|
13
14
|
search: (search: string) => Promise<void>;
|
|
@@ -8,6 +8,7 @@ type DataListRetriever<T> = {
|
|
|
8
8
|
setReady: (state: boolean) => void;
|
|
9
9
|
isLoaded: boolean;
|
|
10
10
|
data: T[] | undefined;
|
|
11
|
+
total?: number;
|
|
11
12
|
next?: (onlyNewRecords?: boolean) => Promise<void>;
|
|
12
13
|
previous?: (onlyNewRecords?: boolean) => Promise<void>;
|
|
13
14
|
search: (search: string) => Promise<void>;
|
package/package.json
CHANGED
|
@@ -51,7 +51,7 @@ export default async function ${names.pascalCase}Page(props: { params: Promise<{
|
|
|
51
51
|
const params = await props.params;
|
|
52
52
|
const ${names.camelCase}: ${names.pascalCase}Interface = await getCached${names.pascalCase}(params.id);
|
|
53
53
|
|
|
54
|
-
ServerSession.checkPermission({ module: Modules.${names.pascalCase}, action: Action.Read, data: ${names.camelCase} });
|
|
54
|
+
await ServerSession.checkPermission({ module: Modules.${names.pascalCase}, action: Action.Read, data: ${names.camelCase} });
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
57
|
<${names.pascalCase}Provider dehydrated${names.pascalCase}={${names.camelCase}.dehydrate()}>
|
|
@@ -23,7 +23,7 @@ import { Action } from "@carlonicora/nextjs-jsonapi/core";
|
|
|
23
23
|
import { ServerSession } from "@carlonicora/nextjs-jsonapi/server";
|
|
24
24
|
|
|
25
25
|
export default async function ${names.pluralPascal}ListPage() {
|
|
26
|
-
ServerSession.checkPermission({ module: Modules.${names.pascalCase}, action: Action.Read });
|
|
26
|
+
await ServerSession.checkPermission({ module: Modules.${names.pascalCase}, action: Action.Read });
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
29
|
<${names.pascalCase}Provider>
|
|
@@ -127,7 +127,7 @@ export const DatePickerPopover = ({
|
|
|
127
127
|
return (
|
|
128
128
|
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
129
129
|
<PopoverTrigger>{children}</PopoverTrigger>
|
|
130
|
-
<PopoverContent className={cn("
|
|
130
|
+
<PopoverContent className={cn("p-0", className)} align={align} onClick={(e) => e.stopPropagation()}>
|
|
131
131
|
<div className="p-3">
|
|
132
132
|
{/* Manual Input */}
|
|
133
133
|
<div className="relative mb-3">
|
|
@@ -169,8 +169,8 @@ export const DatePickerPopover = ({
|
|
|
169
169
|
setDisplayMonth(newDate);
|
|
170
170
|
}}
|
|
171
171
|
>
|
|
172
|
-
<SelectTrigger className="
|
|
173
|
-
<SelectValue
|
|
172
|
+
<SelectTrigger className="flex-1">
|
|
173
|
+
<SelectValue>{monthNames[displayMonth.getMonth()]}</SelectValue>
|
|
174
174
|
</SelectTrigger>
|
|
175
175
|
<SelectContent>
|
|
176
176
|
{monthNames.map((month, index) => (
|
|
@@ -190,8 +190,8 @@ export const DatePickerPopover = ({
|
|
|
190
190
|
setDisplayMonth(newDate);
|
|
191
191
|
}}
|
|
192
192
|
>
|
|
193
|
-
<SelectTrigger className="
|
|
194
|
-
<SelectValue
|
|
193
|
+
<SelectTrigger className="flex-1">
|
|
194
|
+
<SelectValue>{displayMonth.getFullYear()}</SelectValue>
|
|
195
195
|
</SelectTrigger>
|
|
196
196
|
<SelectContent>
|
|
197
197
|
{yearOptions.reverse().map((year) => (
|
|
@@ -204,16 +204,18 @@ export const DatePickerPopover = ({
|
|
|
204
204
|
</div>
|
|
205
205
|
|
|
206
206
|
{/* Calendar */}
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
<div className="flex justify-center">
|
|
208
|
+
<Calendar
|
|
209
|
+
mode="single"
|
|
210
|
+
selected={value}
|
|
211
|
+
onSelect={handleCalendarSelect}
|
|
212
|
+
disabled={(date) => (minDate && date < minDate ? true : false)}
|
|
213
|
+
locale={dateFnsLocale}
|
|
214
|
+
weekStartsOn={1}
|
|
215
|
+
month={displayMonth}
|
|
216
|
+
onMonthChange={setDisplayMonth}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
217
219
|
</div>
|
|
218
220
|
</PopoverContent>
|
|
219
221
|
</Popover>
|
|
@@ -74,7 +74,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
74
74
|
|
|
75
75
|
return (
|
|
76
76
|
<div className="flex w-full flex-col">
|
|
77
|
-
<div className="overflow-
|
|
77
|
+
<div className="overflow-clip rounded-md border">
|
|
78
78
|
<Table>
|
|
79
79
|
<TableHeader className="bg-muted font-semibold">
|
|
80
80
|
{props.title && (
|
|
@@ -140,7 +140,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
140
140
|
</Button>
|
|
141
141
|
{data.pageInfo && (
|
|
142
142
|
<span className="text-muted-foreground text-xs">
|
|
143
|
-
{data.pageInfo.startItem}
|
|
143
|
+
{`${data.pageInfo.startItem}-${data.pageInfo.endItem}${data.total ? ` of ${data.total}` : ""}`}
|
|
144
144
|
</span>
|
|
145
145
|
)}
|
|
146
146
|
<Button
|
|
@@ -21,6 +21,21 @@ export interface SelfRef {
|
|
|
21
21
|
self?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface TotalRef {
|
|
25
|
+
total?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Store the last total from any API call - accessible by hooks
|
|
29
|
+
let lastApiTotal: number | undefined = undefined;
|
|
30
|
+
|
|
31
|
+
export function getLastApiTotal(): number | undefined {
|
|
32
|
+
return lastApiTotal;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function clearLastApiTotal(): void {
|
|
36
|
+
lastApiTotal = undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
let globalErrorHandler: ((status: number, message: string) => void) | null = null;
|
|
25
40
|
|
|
26
41
|
/**
|
|
@@ -79,6 +94,7 @@ export abstract class AbstractService {
|
|
|
79
94
|
next?: NextRef;
|
|
80
95
|
previous?: PreviousRef;
|
|
81
96
|
self?: SelfRef;
|
|
97
|
+
total?: TotalRef;
|
|
82
98
|
}): Promise<T> {
|
|
83
99
|
return await this.callApi<T>({
|
|
84
100
|
method: HttpMethod.GET,
|
|
@@ -87,6 +103,7 @@ export abstract class AbstractService {
|
|
|
87
103
|
next: params.next,
|
|
88
104
|
previous: params.previous,
|
|
89
105
|
self: params.self,
|
|
106
|
+
total: params.total,
|
|
90
107
|
});
|
|
91
108
|
}
|
|
92
109
|
|
|
@@ -99,6 +116,7 @@ export abstract class AbstractService {
|
|
|
99
116
|
next?: NextRef;
|
|
100
117
|
previous?: PreviousRef;
|
|
101
118
|
self?: SelfRef;
|
|
119
|
+
total?: TotalRef;
|
|
102
120
|
}): Promise<T> {
|
|
103
121
|
return await this.callApi<T>({
|
|
104
122
|
method: HttpMethod.GET,
|
|
@@ -107,6 +125,7 @@ export abstract class AbstractService {
|
|
|
107
125
|
next: params.next,
|
|
108
126
|
previous: params.previous,
|
|
109
127
|
self: params.self,
|
|
128
|
+
total: params.total,
|
|
110
129
|
});
|
|
111
130
|
}
|
|
112
131
|
|
|
@@ -123,6 +142,7 @@ export abstract class AbstractService {
|
|
|
123
142
|
next?: NextRef;
|
|
124
143
|
previous?: PreviousRef;
|
|
125
144
|
self?: SelfRef;
|
|
145
|
+
total?: TotalRef;
|
|
126
146
|
responseType?: ApiRequestDataTypeInterface;
|
|
127
147
|
files?: { [key: string]: File | Blob } | File | Blob;
|
|
128
148
|
}): Promise<T> {
|
|
@@ -214,6 +234,11 @@ export abstract class AbstractService {
|
|
|
214
234
|
if (apiResponse.next && params.next) params.next.next = apiResponse.next;
|
|
215
235
|
if (apiResponse.prev && params.previous) params.previous.previous = apiResponse.prev;
|
|
216
236
|
if (apiResponse.self && params.self) params.self.self = apiResponse.self;
|
|
237
|
+
// Always store total for hooks to access, and also populate ref if provided
|
|
238
|
+
if (apiResponse.meta?.total !== undefined) {
|
|
239
|
+
lastApiTotal = apiResponse.meta.total;
|
|
240
|
+
if (params.total) params.total.total = apiResponse.meta.total;
|
|
241
|
+
}
|
|
217
242
|
|
|
218
243
|
return apiResponse.data as T;
|
|
219
244
|
}
|
|
@@ -31,6 +31,10 @@ export interface ClientSelfRef {
|
|
|
31
31
|
self?: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export interface ClientTotalRef {
|
|
35
|
+
total?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
let globalErrorHandler: ((status: number, message: string) => void) | null = null;
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -92,6 +96,7 @@ export abstract class ClientAbstractService {
|
|
|
92
96
|
next?: ClientNextRef;
|
|
93
97
|
previous?: ClientPreviousRef;
|
|
94
98
|
self?: ClientSelfRef;
|
|
99
|
+
total?: ClientTotalRef;
|
|
95
100
|
}): Promise<T> {
|
|
96
101
|
return await this.callApi<T>({
|
|
97
102
|
method: ClientHttpMethod.GET,
|
|
@@ -100,6 +105,7 @@ export abstract class ClientAbstractService {
|
|
|
100
105
|
next: params.next,
|
|
101
106
|
previous: params.previous,
|
|
102
107
|
self: params.self,
|
|
108
|
+
total: params.total,
|
|
103
109
|
});
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -112,6 +118,7 @@ export abstract class ClientAbstractService {
|
|
|
112
118
|
next?: ClientNextRef;
|
|
113
119
|
previous?: ClientPreviousRef;
|
|
114
120
|
self?: ClientSelfRef;
|
|
121
|
+
total?: ClientTotalRef;
|
|
115
122
|
}): Promise<T> {
|
|
116
123
|
return await this.callApi<T>({
|
|
117
124
|
method: ClientHttpMethod.GET,
|
|
@@ -120,6 +127,7 @@ export abstract class ClientAbstractService {
|
|
|
120
127
|
next: params.next,
|
|
121
128
|
previous: params.previous,
|
|
122
129
|
self: params.self,
|
|
130
|
+
total: params.total,
|
|
123
131
|
});
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -136,6 +144,7 @@ export abstract class ClientAbstractService {
|
|
|
136
144
|
next?: ClientNextRef;
|
|
137
145
|
previous?: ClientPreviousRef;
|
|
138
146
|
self?: ClientSelfRef;
|
|
147
|
+
total?: ClientTotalRef;
|
|
139
148
|
responseType?: ApiRequestDataTypeInterface;
|
|
140
149
|
files?: { [key: string]: File | Blob } | File | Blob;
|
|
141
150
|
}): Promise<T> {
|
|
@@ -216,6 +225,7 @@ export abstract class ClientAbstractService {
|
|
|
216
225
|
if (apiResponse.next && params.next) params.next.next = apiResponse.next;
|
|
217
226
|
if (apiResponse.prev && params.previous) params.previous.previous = apiResponse.prev;
|
|
218
227
|
if (apiResponse.self && params.self) params.self.self = apiResponse.self;
|
|
228
|
+
if (apiResponse.meta?.total !== undefined && params.total) params.total.total = apiResponse.meta.total;
|
|
219
229
|
|
|
220
230
|
return apiResponse.data as T;
|
|
221
231
|
}
|
|
@@ -442,7 +442,10 @@ export function BillingDashboardContainer() {
|
|
|
442
442
|
onOpenChange={handleModalClose}
|
|
443
443
|
title={getModalTitle("subscriptions")}
|
|
444
444
|
>
|
|
445
|
-
<SubscriptionsContainer
|
|
445
|
+
<SubscriptionsContainer
|
|
446
|
+
onOpenWizard={handleOpenWizard}
|
|
447
|
+
hasActiveRecurringSubscription={hasActiveRecurringSubscription}
|
|
448
|
+
/>
|
|
446
449
|
</BillingDetailModal>
|
|
447
450
|
|
|
448
451
|
<BillingDetailModal
|
|
@@ -86,7 +86,7 @@ export function InvoiceDetails({ invoice, open, onOpenChange, onInvoiceChange }:
|
|
|
86
86
|
{/* Line Items */}
|
|
87
87
|
<div>
|
|
88
88
|
<h4 className="text-sm font-medium text-muted-foreground mb-2">Line Items</h4>
|
|
89
|
-
<div className="border rounded-lg overflow-
|
|
89
|
+
<div className="border rounded-lg overflow-clip">
|
|
90
90
|
<table className="w-full">
|
|
91
91
|
<thead className="bg-muted">
|
|
92
92
|
<tr>
|
|
@@ -29,7 +29,7 @@ export function InvoicesList({ invoices, onInvoicesChange }: InvoicesListProps)
|
|
|
29
29
|
|
|
30
30
|
return (
|
|
31
31
|
<>
|
|
32
|
-
<div className="border rounded-lg overflow-
|
|
32
|
+
<div className="border rounded-lg overflow-clip">
|
|
33
33
|
<Table>
|
|
34
34
|
<TableHeader className="bg-muted">
|
|
35
35
|
<TableRow>
|
|
@@ -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}
|