@carlonicora/nextjs-jsonapi 1.28.0 → 1.29.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 (95) hide show
  1. package/dist/{BlockNoteEditor-CAUNVZUF.js → BlockNoteEditor-YBVEOPV4.js} +13 -13
  2. package/dist/{BlockNoteEditor-CAUNVZUF.js.map → BlockNoteEditor-YBVEOPV4.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-EOA4OEVX.mjs → BlockNoteEditor-ZM4YPXHO.mjs} +3 -3
  4. package/dist/billing/index.d.mts +47 -17
  5. package/dist/billing/index.d.ts +47 -17
  6. package/dist/billing/index.js +1241 -1073
  7. package/dist/billing/index.js.map +1 -1
  8. package/dist/billing/index.mjs +1375 -1207
  9. package/dist/billing/index.mjs.map +1 -1
  10. package/dist/{chunk-IXI4GAKB.js → chunk-3X7EEFMN.js} +488 -431
  11. package/dist/chunk-3X7EEFMN.js.map +1 -0
  12. package/dist/{chunk-ORFXBO7F.mjs → chunk-DU64WMZD.mjs} +6 -3
  13. package/dist/chunk-DU64WMZD.mjs.map +1 -0
  14. package/dist/{chunk-TSEU4KZ2.js → chunk-J22NEVSK.js} +21 -18
  15. package/dist/chunk-J22NEVSK.js.map +1 -0
  16. package/dist/{chunk-PYASRX75.mjs → chunk-UCD5CUE4.mjs} +81 -24
  17. package/dist/chunk-UCD5CUE4.mjs.map +1 -0
  18. package/dist/client/index.d.mts +14 -5
  19. package/dist/client/index.d.ts +14 -5
  20. package/dist/client/index.js +5 -3
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/index.mjs +4 -2
  23. package/dist/components/index.d.mts +2 -2
  24. package/dist/components/index.d.ts +2 -2
  25. package/dist/components/index.js +3 -3
  26. package/dist/components/index.mjs +2 -2
  27. package/dist/{config-B4pZpLT9.d.ts → config-CHwoRDOp.d.ts} +1 -1
  28. package/dist/{config-DT1K-t6I.d.mts → config-DiWyJzk9.d.mts} +1 -1
  29. package/dist/{content.interface-B2Ldg0vg.d.mts → content.interface-BSpowEiW.d.mts} +1 -1
  30. package/dist/{content.interface-D8NHv3DX.d.ts → content.interface-DFQ7mkpL.d.ts} +1 -1
  31. package/dist/contexts/index.d.mts +2 -2
  32. package/dist/contexts/index.d.ts +2 -2
  33. package/dist/contexts/index.js +3 -3
  34. package/dist/contexts/index.mjs +2 -2
  35. package/dist/core/index.d.mts +39 -37
  36. package/dist/core/index.d.ts +39 -37
  37. package/dist/core/index.js +2 -2
  38. package/dist/core/index.mjs +1 -1
  39. package/dist/index.d.mts +4 -4
  40. package/dist/index.d.ts +4 -4
  41. package/dist/index.js +2 -2
  42. package/dist/index.mjs +1 -1
  43. package/dist/{notification.interface-H0L9WBge.d.ts → notification.interface-CmKmObIU.d.ts} +1 -0
  44. package/dist/{notification.interface-DEn-Yp_b.d.mts → notification.interface-D5MbtfZK.d.mts} +1 -0
  45. package/dist/{s3.service-BNytYanU.d.mts → s3.service-BMT7W6KS.d.mts} +19 -19
  46. package/dist/{s3.service-C7f_Ygz5.d.ts → s3.service-DsXo9nop.d.ts} +19 -19
  47. package/dist/server/index.d.mts +3 -3
  48. package/dist/server/index.d.ts +3 -3
  49. package/dist/server/index.js +3 -3
  50. package/dist/server/index.mjs +1 -1
  51. package/dist/{useSocket-BcnThTD0.d.mts → useSocket-DUqGoPya.d.mts} +1 -1
  52. package/dist/{useSocket-QZTOCzRF.d.ts → useSocket-QuHa0ZmO.d.ts} +1 -1
  53. package/package.json +1 -1
  54. package/src/client/index.ts +1 -0
  55. package/src/components/forms/FormSelect.tsx +2 -1
  56. package/src/features/auth/data/auth.ts +0 -2
  57. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +60 -3
  58. package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +12 -152
  59. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +168 -0
  60. package/src/features/billing/stripe-customer/components/forms/index.ts +1 -0
  61. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +19 -1
  62. package/src/features/billing/stripe-product/components/forms/ProductEditor.tsx +2 -2
  63. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -235
  64. package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +7 -18
  65. package/src/features/billing/stripe-subscription/components/forms/index.ts +0 -1
  66. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +10 -1
  67. package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +28 -0
  68. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +128 -0
  69. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +54 -0
  70. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +68 -0
  71. package/src/features/billing/stripe-subscription/components/widgets/index.ts +4 -1
  72. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +114 -0
  73. package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +66 -0
  74. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +32 -0
  75. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +103 -0
  76. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +133 -0
  77. package/src/features/billing/stripe-subscription/components/wizards/index.ts +6 -0
  78. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +217 -0
  79. package/src/features/billing/stripe-subscription/index.ts +3 -2
  80. package/src/features/company/components/details/TokenStatusIndicator.tsx +19 -9
  81. package/src/features/company/data/company.interface.ts +2 -0
  82. package/src/features/company/data/company.ts +7 -0
  83. package/src/features/company/hooks/index.ts +1 -0
  84. package/src/features/company/hooks/useSubscriptionStatus.ts +71 -0
  85. package/src/features/user/components/forms/UserEditor.tsx +1 -1
  86. package/src/features/user/components/lists/AdminUsersList.tsx +1 -1
  87. package/src/features/user/contexts/CurrentUserContext.tsx +1 -1
  88. package/src/features/user/data/user.ts +1 -1
  89. package/dist/chunk-IXI4GAKB.js.map +0 -1
  90. package/dist/chunk-ORFXBO7F.mjs.map +0 -1
  91. package/dist/chunk-PYASRX75.mjs.map +0 -1
  92. package/dist/chunk-TSEU4KZ2.js.map +0 -1
  93. package/src/features/billing/stripe-subscription/components/forms/SubscriptionEditor.tsx +0 -331
  94. package/src/features/billing/stripe-subscription/components/widgets/PricingCardsGrid.tsx +0 -110
  95. /package/dist/{BlockNoteEditor-EOA4OEVX.mjs.map → BlockNoteEditor-ZM4YPXHO.mjs.map} +0 -0
@@ -1,45 +1,21 @@
1
1
  "use client";
2
2
 
3
- import { CheckCircle, CreditCard, Loader2 } from "lucide-react";
4
- import { useEffect, useState } from "react";
5
- import { v4 } from "uuid";
6
- import { Alert, AlertDescription, AlertTitle, Button } from "../../../../../shadcnui";
3
+ import { CreditCard } from "lucide-react";
4
+ import { useCallback, useEffect, useState } from "react";
5
+ import { Button } from "../../../../../shadcnui";
7
6
  import { BillingAlertBanner } from "../../../components";
8
- import { PaymentMethodEditor } from "../../../stripe-customer/components/forms/PaymentMethodEditor";
9
- import { StripeCustomerService } from "../../../stripe-customer/data/stripe-customer.service";
10
- import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
11
- import { StripeProductInterface, StripeProductService } from "../../../stripe-product";
12
7
  import { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from "../../data";
13
- import { useConfirmSubscriptionPayment } from "../../hooks";
14
- import { SubscriptionEditor } from "../forms";
15
8
  import { SubscriptionsList } from "../lists";
16
- import { PricesByProduct, PricingCardsGrid } from "../widgets/PricingCardsGrid";
17
9
 
18
- type PaymentConfirmationState = "idle" | "confirming" | "success" | "error";
19
-
20
- export function SubscriptionsContainer() {
21
- const { confirmPayment, isConfirming } = useConfirmSubscriptionPayment();
10
+ type SubscriptionsContainerProps = {
11
+ onOpenWizard?: (subscription?: StripeSubscriptionInterface) => void;
12
+ };
22
13
 
14
+ export function SubscriptionsContainer({ onOpenWizard }: SubscriptionsContainerProps) {
23
15
  const [subscriptions, setSubscriptions] = useState<StripeSubscriptionInterface[]>([]);
24
16
  const [loading, setLoading] = useState<boolean>(true);
25
- const [showCreateSubscription, setShowCreateSubscription] = useState<boolean>(false);
26
- const [showPaymentMethodEditor, setShowPaymentMethodEditor] = useState<boolean>(false);
27
-
28
- // Pricing data for empty state
29
- const [products, setProducts] = useState<StripeProductInterface[]>([]);
30
- const [pricesByProduct, setPricesByProduct] = useState<PricesByProduct>(new Map());
31
- const [loadingPricing, setLoadingPricing] = useState<boolean>(false);
32
-
33
- // Payment method and pending subscription state
34
- const [hasPaymentMethod, setHasPaymentMethod] = useState<boolean | null>(null);
35
- const [pendingPriceId, setPendingPriceId] = useState<string | null>(null);
36
- const [creatingSubscription, setCreatingSubscription] = useState<boolean>(false);
37
17
 
38
- // Payment confirmation state
39
- const [paymentConfirmationState, setPaymentConfirmationState] = useState<PaymentConfirmationState>("idle");
40
- const [paymentError, setPaymentError] = useState<string | null>(null);
41
-
42
- const loadSubscriptions = async () => {
18
+ const loadSubscriptions = useCallback(async () => {
43
19
  setLoading(true);
44
20
  try {
45
21
  const fetchedSubscriptions = await StripeSubscriptionService.listSubscriptions();
@@ -49,125 +25,12 @@ export function SubscriptionsContainer() {
49
25
  } finally {
50
26
  setLoading(false);
51
27
  }
52
- };
53
-
54
- const loadPricingData = async () => {
55
- setLoadingPricing(true);
56
- try {
57
- const fetchedProducts = await StripeProductService.listProducts({ active: true });
58
-
59
- // Build prices map from product.stripePrices
60
- const grouped: PricesByProduct = new Map();
61
- for (const product of fetchedProducts) {
62
- if (product.stripePrices && product.stripePrices.length > 0) {
63
- grouped.set(product.id, product.stripePrices);
64
- }
65
- }
66
-
67
- setProducts(fetchedProducts);
68
- setPricesByProduct(grouped);
69
- } catch (error) {
70
- console.error("[SubscriptionsContainer] Failed to load pricing data:", error);
71
- } finally {
72
- setLoadingPricing(false);
73
- }
74
- };
75
-
76
- const checkPaymentMethod = async () => {
77
- try {
78
- const paymentMethods = await StripeCustomerService.listPaymentMethods();
79
- const hasMethod = paymentMethods.length > 0;
80
- setHasPaymentMethod(hasMethod);
81
- } catch (error) {
82
- console.error("[SubscriptionsContainer] Failed to check payment methods:", error);
83
- setHasPaymentMethod(false);
84
- }
85
- };
86
-
87
- const createSubscriptionWithPrice = async (priceId: string) => {
88
- setCreatingSubscription(true);
89
- setPaymentError(null);
90
- setPaymentConfirmationState("idle");
91
-
92
- try {
93
- const result = await StripeSubscriptionService.createSubscription({
94
- id: v4(),
95
- priceId,
96
- });
97
-
98
- // Check if payment confirmation is required (SCA flow)
99
- if (result.meta.requiresAction && result.meta.clientSecret) {
100
- setPaymentConfirmationState("confirming");
101
-
102
- const confirmation = await confirmPayment(result.meta.clientSecret);
103
-
104
- if (!confirmation.success) {
105
- console.error("[SubscriptionsContainer] Payment confirmation failed:", confirmation.error);
106
- setPaymentConfirmationState("error");
107
- setPaymentError(confirmation.error || "Payment confirmation failed");
108
- setCreatingSubscription(false);
109
- return;
110
- }
111
-
112
- // Sync subscription to get updated status from Stripe
113
- await StripeSubscriptionService.syncSubscription({
114
- subscriptionId: result.subscription.id,
115
- });
116
- }
117
-
118
- // Success
119
- setPaymentConfirmationState("success");
120
- await loadSubscriptions();
121
- } catch (error: any) {
122
- console.error("[SubscriptionsContainer] Failed to create subscription:", error);
123
- // Handle 402 error - payment method required despite our check
124
- if (error?.status === 402 || error?.response?.status === 402) {
125
- setPendingPriceId(priceId);
126
- setHasPaymentMethod(false);
127
- setShowPaymentMethodEditor(true);
128
- } else {
129
- setPaymentConfirmationState("error");
130
- setPaymentError(error?.message || "Failed to create subscription");
131
- }
132
- } finally {
133
- setCreatingSubscription(false);
134
- }
135
- };
136
-
137
- const handleSelectPrice = async (price: StripePriceInterface) => {
138
- const priceId = price.id; // Use internal UUID, not Stripe ID
139
-
140
- if (!hasPaymentMethod) {
141
- setPendingPriceId(priceId);
142
- setShowPaymentMethodEditor(true);
143
- return;
144
- }
145
-
146
- await createSubscriptionWithPrice(priceId);
147
- };
148
-
149
- const handlePaymentMethodSuccess = async () => {
150
- setShowPaymentMethodEditor(false);
151
- setHasPaymentMethod(true);
152
-
153
- if (pendingPriceId) {
154
- await createSubscriptionWithPrice(pendingPriceId);
155
- setPendingPriceId(null);
156
- }
157
- };
28
+ }, []);
158
29
 
159
30
  useEffect(() => {
160
31
  loadSubscriptions();
161
32
  }, []);
162
33
 
163
- // Load pricing data when there are no subscriptions
164
- useEffect(() => {
165
- if (!loading && subscriptions.length === 0) {
166
- loadPricingData();
167
- checkPaymentMethod();
168
- }
169
- }, [loading, subscriptions.length]);
170
-
171
34
  // Detect critical subscriptions
172
35
  const criticalSubscriptions = subscriptions.filter(
173
36
  (sub) =>
@@ -194,7 +57,7 @@ export function SubscriptionsContainer() {
194
57
  <h1 className="text-3xl font-bold">Subscriptions</h1>
195
58
  </div>
196
59
  {subscriptions.length > 0 && (
197
- <Button onClick={() => setShowCreateSubscription(true)}>Subscribe to a Plan</Button>
60
+ <Button onClick={() => onOpenWizard?.()}>Subscribe to a Plan</Button>
198
61
  )}
199
62
  </div>
200
63
 
@@ -203,100 +66,26 @@ export function SubscriptionsContainer() {
203
66
  <BillingAlertBanner key={subscription.id} subscription={subscription} />
204
67
  ))}
205
68
 
206
- {/* Pricing Cards when no subscriptions */}
69
+ {/* Empty state CTA */}
207
70
  {subscriptions.length === 0 && (
208
- <div className="space-y-6">
209
- {/* Payment confirmation states */}
210
- {(paymentConfirmationState === "confirming" || isConfirming) && (
211
- <div className="flex flex-col items-center justify-center py-12 space-y-4 bg-muted/50 rounded-lg">
212
- <Loader2 className="h-8 w-8 animate-spin text-primary" />
213
- <div className="text-center">
214
- <p className="font-medium">Processing payment...</p>
215
- <p className="text-sm text-muted-foreground">Please complete any verification if prompted.</p>
216
- </div>
217
- </div>
218
- )}
219
-
220
- {paymentConfirmationState === "success" && (
221
- <div className="flex flex-col items-center justify-center py-8 space-y-4 bg-green-50 rounded-lg border border-green-200">
222
- <CheckCircle className="h-12 w-12 text-green-500" />
223
- <div className="text-center">
224
- <p className="font-medium text-green-600">Payment successful!</p>
225
- <p className="text-sm text-muted-foreground">Your subscription is now active.</p>
226
- </div>
227
- </div>
228
- )}
229
-
230
- {paymentConfirmationState === "error" && (
231
- <Alert variant="destructive">
232
- <AlertTitle>Payment Failed</AlertTitle>
233
- <AlertDescription className="mt-2">
234
- <p className="mb-4">{paymentError || "We couldn't process your payment. Please try again."}</p>
235
- <Button
236
- onClick={() => {
237
- setPaymentConfirmationState("idle");
238
- setPaymentError(null);
239
- }}
240
- variant="outline"
241
- >
242
- Try Again
243
- </Button>
244
- </AlertDescription>
245
- </Alert>
246
- )}
247
-
248
- {paymentConfirmationState === "idle" && !isConfirming && (
249
- <>
250
- <div className="text-center">
251
- <CreditCard className="text-muted-foreground mx-auto h-16 w-16 mb-4" />
252
- <h3 className="mb-2 text-xl font-semibold">Choose Your Plan</h3>
253
- <p className="text-muted-foreground mb-6">
254
- Select a subscription plan to get started with our services.
255
- </p>
256
- </div>
257
-
258
- <PricingCardsGrid
259
- products={products}
260
- pricesByProduct={pricesByProduct}
261
- loading={loadingPricing}
262
- loadingPriceId={creatingSubscription ? (pendingPriceId ?? undefined) : undefined}
263
- onSelectPrice={handleSelectPrice}
264
- />
265
- </>
266
- )}
71
+ <div className="flex flex-col items-center justify-center py-12 space-y-4">
72
+ <CreditCard className="h-16 w-16 text-muted-foreground" />
73
+ <div className="text-center">
74
+ <h3 className="text-xl font-semibold mb-2">No Active Subscriptions</h3>
75
+ <p className="text-muted-foreground mb-6">
76
+ Choose a subscription plan to get started with our services.
77
+ </p>
78
+ <Button onClick={() => onOpenWizard?.()}>Subscribe to a Plan</Button>
79
+ </div>
267
80
  </div>
268
81
  )}
269
82
 
270
83
  {/* Subscriptions List */}
271
84
  {subscriptions.length > 0 && (
272
- <SubscriptionsList subscriptions={subscriptions} onSubscriptionsChange={loadSubscriptions} />
273
- )}
274
-
275
- {/* Create Subscription Modal (for users who already have subscriptions) */}
276
- {showCreateSubscription && (
277
- <SubscriptionEditor
278
- open={showCreateSubscription}
279
- onOpenChange={setShowCreateSubscription}
280
- onSuccess={loadSubscriptions}
281
- onAddPaymentMethod={() => {
282
- setShowCreateSubscription(false);
283
- setShowPaymentMethodEditor(true);
284
- }}
285
- />
286
- )}
287
-
288
- {/* Payment Method Editor Modal */}
289
- {showPaymentMethodEditor && (
290
- <PaymentMethodEditor
291
- open={showPaymentMethodEditor}
292
- onOpenChange={(open) => {
293
- setShowPaymentMethodEditor(open);
294
- if (!open) {
295
- // Clear pending price if user cancels
296
- setPendingPriceId(null);
297
- }
298
- }}
299
- onSuccess={handlePaymentMethodSuccess}
85
+ <SubscriptionsList
86
+ subscriptions={subscriptions}
87
+ onSubscriptionsChange={loadSubscriptions}
88
+ onChangePlan={(sub) => onOpenWizard?.(sub)}
300
89
  />
301
90
  )}
302
91
  </div>
@@ -7,7 +7,6 @@ import { StripeCustomerService } from "../../../stripe-customer";
7
7
  import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
8
8
  import { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from "../../data";
9
9
  import { CancelSubscriptionDialog } from "../forms/CancelSubscriptionDialog";
10
- import { SubscriptionEditor } from "../forms/SubscriptionEditor";
11
10
  import { SubscriptionStatusBadge } from "../widgets/SubscriptionStatusBadge";
12
11
 
13
12
  /**
@@ -53,6 +52,7 @@ type SubscriptionDetailsProps = {
53
52
  open: boolean;
54
53
  onOpenChange: (open: boolean) => void;
55
54
  onSubscriptionChange: () => void;
55
+ onChangePlan?: (subscription: StripeSubscriptionInterface) => void;
56
56
  };
57
57
 
58
58
  export function SubscriptionDetails({
@@ -60,8 +60,8 @@ export function SubscriptionDetails({
60
60
  open,
61
61
  onOpenChange,
62
62
  onSubscriptionChange,
63
+ onChangePlan,
63
64
  }: SubscriptionDetailsProps) {
64
- const [showEdit, setShowEdit] = useState<boolean>(false);
65
65
  const [showCancel, setShowCancel] = useState<boolean>(false);
66
66
  const [isProcessing, setIsProcessing] = useState<boolean>(false);
67
67
 
@@ -163,9 +163,11 @@ export function SubscriptionDetails({
163
163
 
164
164
  {/* Action Buttons */}
165
165
  <div className="flex flex-wrap gap-2 pt-4 border-t">
166
- <Button variant="default" onClick={() => setShowEdit(true)}>
167
- Change Plan
168
- </Button>
166
+ {onChangePlan && (
167
+ <Button variant="default" onClick={() => onChangePlan(subscription)}>
168
+ Change Plan
169
+ </Button>
170
+ )}
169
171
 
170
172
  {canPause && (
171
173
  <Button variant="outline" onClick={handlePause} disabled={isProcessing}>
@@ -193,19 +195,6 @@ export function SubscriptionDetails({
193
195
  </DialogContent>
194
196
  </Dialog>
195
197
 
196
- {/* Edit Subscription Dialog */}
197
- {showEdit && (
198
- <SubscriptionEditor
199
- subscription={subscription}
200
- open={showEdit}
201
- onOpenChange={setShowEdit}
202
- onSuccess={() => {
203
- onSubscriptionChange();
204
- setShowEdit(false);
205
- }}
206
- />
207
- )}
208
-
209
198
  {/* Cancel Subscription Dialog */}
210
199
  {showCancel && (
211
200
  <CancelSubscriptionDialog
@@ -1,2 +1 @@
1
1
  export * from "./CancelSubscriptionDialog";
2
- export * from "./SubscriptionEditor";
@@ -41,9 +41,10 @@ function formatPlanName(price: StripePriceInterface | undefined): string {
41
41
  type SubscriptionsListProps = {
42
42
  subscriptions: StripeSubscriptionInterface[];
43
43
  onSubscriptionsChange: () => void;
44
+ onChangePlan?: (subscription: StripeSubscriptionInterface) => void;
44
45
  };
45
46
 
46
- export function SubscriptionsList({ subscriptions, onSubscriptionsChange }: SubscriptionsListProps) {
47
+ export function SubscriptionsList({ subscriptions, onSubscriptionsChange, onChangePlan }: SubscriptionsListProps) {
47
48
  const [selectedSub, setSelectedSub] = useState<StripeSubscriptionInterface | null>(null);
48
49
 
49
50
  const handleRowClick = (subscription: StripeSubscriptionInterface) => {
@@ -97,6 +98,14 @@ export function SubscriptionsList({ subscriptions, onSubscriptionsChange }: Subs
97
98
  onSubscriptionsChange();
98
99
  setSelectedSub(null);
99
100
  }}
101
+ onChangePlan={
102
+ onChangePlan
103
+ ? (sub) => {
104
+ setSelectedSub(null); // Close details dialog first
105
+ onChangePlan(sub); // Then open wizard at parent level
106
+ }
107
+ : undefined
108
+ }
100
109
  />
101
110
  )}
102
111
  </>
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import { Tabs, TabsList, TabsTrigger } from "../../../../../shadcnui";
4
+
5
+ export type BillingInterval = "month" | "year";
6
+
7
+ export type IntervalToggleProps = {
8
+ value: BillingInterval;
9
+ onChange: (interval: BillingInterval) => void;
10
+ hasMonthly: boolean;
11
+ hasYearly: boolean;
12
+ };
13
+
14
+ export function IntervalToggle({ value, onChange, hasMonthly, hasYearly }: IntervalToggleProps) {
15
+ // Only render if BOTH intervals are available
16
+ if (!hasMonthly || !hasYearly) {
17
+ return null;
18
+ }
19
+
20
+ return (
21
+ <Tabs value={value} onValueChange={(v) => onChange(v as BillingInterval)}>
22
+ <TabsList>
23
+ <TabsTrigger value="month">Monthly</TabsTrigger>
24
+ <TabsTrigger value="year">Yearly</TabsTrigger>
25
+ </TabsList>
26
+ </Tabs>
27
+ );
28
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { Skeleton } from "../../../../../shadcnui";
4
+ import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
5
+ import { StripeProductInterface } from "../../../stripe-product";
6
+ import { BillingInterval } from "./IntervalToggle";
7
+ import { ProductPricingRow } from "./ProductPricingRow";
8
+
9
+ export type ProductPricingListProps = {
10
+ products: StripeProductInterface[];
11
+ selectedInterval: BillingInterval;
12
+ currentPriceId?: string;
13
+ selectedPriceId?: string;
14
+ loadingPriceId?: string;
15
+ loading?: boolean;
16
+ onSelectPrice: (price: StripePriceInterface) => void;
17
+ hideRecurringPrices?: boolean;
18
+ };
19
+
20
+ function isRecurringProduct(prices: StripePriceInterface[]): boolean {
21
+ return prices.some((p) => p.priceType === "recurring");
22
+ }
23
+
24
+ function getFilteredPrices(prices: StripePriceInterface[], selectedInterval: BillingInterval): StripePriceInterface[] {
25
+ const isRecurring = isRecurringProduct(prices);
26
+
27
+ if (!isRecurring) {
28
+ return prices.filter((p) => p.priceType === "one_time");
29
+ }
30
+
31
+ const intervalPrices = prices.filter(
32
+ (p) => p.priceType === "recurring" && p.recurring?.interval === selectedInterval,
33
+ );
34
+
35
+ if (intervalPrices.length === 0) {
36
+ const fallbackInterval = selectedInterval === "month" ? "year" : "month";
37
+ return prices.filter((p) => p.priceType === "recurring" && p.recurring?.interval === fallbackInterval);
38
+ }
39
+
40
+ return intervalPrices;
41
+ }
42
+
43
+ export function ProductPricingList({
44
+ products,
45
+ selectedInterval,
46
+ currentPriceId,
47
+ selectedPriceId,
48
+ loadingPriceId,
49
+ loading = false,
50
+ onSelectPrice,
51
+ hideRecurringPrices = false,
52
+ }: ProductPricingListProps) {
53
+ if (loading) {
54
+ return <ProductPricingListSkeleton />;
55
+ }
56
+
57
+ if (products.length === 0) {
58
+ return <div className="text-center py-8 text-muted-foreground">No plans available</div>;
59
+ }
60
+
61
+ const sortedProducts = [...products].sort((a, b) => {
62
+ const aRecurring = isRecurringProduct(a.stripePrices || []);
63
+ const bRecurring = isRecurringProduct(b.stripePrices || []);
64
+ if (aRecurring && !bRecurring) return -1;
65
+ if (!aRecurring && bRecurring) return 1;
66
+ return 0;
67
+ });
68
+
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
+ return (
80
+ <div className="space-y-6">
81
+ {filteredProducts.map((product) => {
82
+ const allPrices = product.stripePrices || [];
83
+ const filteredPrices = getFilteredPrices(allPrices, selectedInterval);
84
+
85
+ if (filteredPrices.length === 0) {
86
+ return null;
87
+ }
88
+
89
+ return (
90
+ <ProductPricingRow
91
+ key={product.id}
92
+ product={product}
93
+ prices={filteredPrices}
94
+ currentPriceId={currentPriceId}
95
+ selectedPriceId={selectedPriceId}
96
+ loadingPriceId={loadingPriceId}
97
+ onSelectPrice={onSelectPrice}
98
+ />
99
+ );
100
+ })}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function ProductPricingListSkeleton() {
106
+ return (
107
+ <div className="space-y-6">
108
+ {[1, 2].map((rowIndex) => (
109
+ <div key={rowIndex} className="space-y-3">
110
+ <Skeleton className="h-6 w-32" />
111
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
112
+ {[1, 2, 3].map((cardIndex) => (
113
+ <div key={cardIndex} className="p-4 rounded-lg border animate-pulse space-y-3">
114
+ <Skeleton className="h-6 w-24" />
115
+ <Skeleton className="h-8 w-32" />
116
+ <div className="space-y-2">
117
+ <Skeleton className="h-4 w-full" />
118
+ <Skeleton className="h-4 w-3/4" />
119
+ </div>
120
+ <Skeleton className="h-10 w-full" />
121
+ </div>
122
+ ))}
123
+ </div>
124
+ </div>
125
+ ))}
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
4
+ import { StripeProductInterface } from "../../../stripe-product";
5
+ import { PricingCard } from "./PricingCard";
6
+
7
+ export type ProductPricingRowProps = {
8
+ product: StripeProductInterface;
9
+ prices: StripePriceInterface[]; // Multiple prices for this product
10
+ currentPriceId?: string;
11
+ selectedPriceId?: string;
12
+ loadingPriceId?: string;
13
+ onSelectPrice: (price: StripePriceInterface) => void;
14
+ };
15
+
16
+ export function ProductPricingRow({
17
+ product,
18
+ prices,
19
+ currentPriceId,
20
+ selectedPriceId,
21
+ loadingPriceId,
22
+ onSelectPrice,
23
+ }: ProductPricingRowProps) {
24
+ if (prices.length === 0) {
25
+ return null;
26
+ }
27
+
28
+ return (
29
+ <div className="space-y-3">
30
+ {/* Product name header */}
31
+ <h3 className="font-semibold text-lg">{product.name}</h3>
32
+
33
+ {/* Price cards in columns */}
34
+ <div
35
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
36
+ role="radiogroup"
37
+ aria-label={`Pricing options for ${product.name}`}
38
+ >
39
+ {prices
40
+ .sort((a, b) => (a.unitAmount ?? 0) - (b.unitAmount ?? 0))
41
+ .map((price) => (
42
+ <PricingCard
43
+ key={price.id}
44
+ price={price}
45
+ isCurrentPlan={price.id === currentPriceId}
46
+ isSelected={price.id === selectedPriceId}
47
+ isLoading={price.id === loadingPriceId}
48
+ onSelect={onSelectPrice}
49
+ />
50
+ ))}
51
+ </div>
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ import { Check, Loader2 } from "lucide-react";
4
+ import { Button } from "../../../../../shadcnui";
5
+ import { formatCurrency, formatInterval } from "../../../components/utils";
6
+ import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
7
+
8
+ type SubscriptionConfirmationProps = {
9
+ price: StripePriceInterface;
10
+ isLoading: boolean;
11
+ onConfirm: () => void;
12
+ onCancel: () => void;
13
+ };
14
+
15
+ export function SubscriptionConfirmation({ price, isLoading, onConfirm, onCancel }: SubscriptionConfirmationProps) {
16
+ const productName = price.product?.name || price.nickname || "Selected Plan";
17
+ const productDescription = price.product?.description || price.description;
18
+ const features = price.features || [];
19
+
20
+ return (
21
+ <div className="bg-accent/10 border border-accent/30 rounded-lg p-4">
22
+ <h4 className="font-semibold mb-3">Confirm Your Subscription</h4>
23
+
24
+ <div className="space-y-3">
25
+ {/* Plan name and description */}
26
+ <div>
27
+ <div className="font-medium">{productName}</div>
28
+ {productDescription && <div className="text-sm text-muted-foreground">{productDescription}</div>}
29
+ </div>
30
+
31
+ {/* Price */}
32
+ <div className="text-lg font-semibold">
33
+ {formatCurrency(price.unitAmount, price.currency)}
34
+ <span className="text-sm font-normal text-muted-foreground">{formatInterval(price)}</span>
35
+ </div>
36
+
37
+ {/* Features */}
38
+ {features.length > 0 && (
39
+ <div className="space-y-1">
40
+ {features.map((feature, index) => (
41
+ <div key={index} className="flex items-center gap-2 text-sm ">
42
+ <Check className="h-4 w-4 text-primary" />
43
+ <span>{feature}</span>
44
+ </div>
45
+ ))}
46
+ </div>
47
+ )}
48
+
49
+ {/* Action buttons */}
50
+ <div className="flex justify-end gap-3 pt-2 border-t border-accent/30">
51
+ <Button variant="outline" onClick={onCancel} disabled={isLoading}>
52
+ Cancel
53
+ </Button>
54
+ <Button onClick={onConfirm} disabled={isLoading}>
55
+ {isLoading ? (
56
+ <>
57
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
58
+ Processing...
59
+ </>
60
+ ) : (
61
+ "Subscribe"
62
+ )}
63
+ </Button>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ );
68
+ }