@carlonicora/nextjs-jsonapi 1.28.0 → 1.29.1

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 (96) hide show
  1. package/dist/{BlockNoteEditor-CAUNVZUF.js → BlockNoteEditor-7BDLLHRA.js} +13 -13
  2. package/dist/{BlockNoteEditor-CAUNVZUF.js.map → BlockNoteEditor-7BDLLHRA.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-EOA4OEVX.mjs → BlockNoteEditor-F5KCNLVF.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-7M7NPKOF.js} +490 -433
  11. package/dist/chunk-7M7NPKOF.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-YLSLXQ3O.mjs} +83 -26
  17. package/dist/chunk-YLSLXQ3O.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/components/pages/PageContentContainer.tsx +2 -2
  57. package/src/features/auth/data/auth.ts +0 -2
  58. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +60 -3
  59. package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +12 -152
  60. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +168 -0
  61. package/src/features/billing/stripe-customer/components/forms/index.ts +1 -0
  62. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +19 -1
  63. package/src/features/billing/stripe-product/components/forms/ProductEditor.tsx +2 -2
  64. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -235
  65. package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +7 -18
  66. package/src/features/billing/stripe-subscription/components/forms/index.ts +0 -1
  67. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +10 -1
  68. package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +28 -0
  69. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +128 -0
  70. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +54 -0
  71. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +68 -0
  72. package/src/features/billing/stripe-subscription/components/widgets/index.ts +4 -1
  73. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +114 -0
  74. package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +66 -0
  75. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +32 -0
  76. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +103 -0
  77. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +133 -0
  78. package/src/features/billing/stripe-subscription/components/wizards/index.ts +6 -0
  79. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +217 -0
  80. package/src/features/billing/stripe-subscription/index.ts +3 -2
  81. package/src/features/company/components/details/TokenStatusIndicator.tsx +19 -9
  82. package/src/features/company/data/company.interface.ts +2 -0
  83. package/src/features/company/data/company.ts +7 -0
  84. package/src/features/company/hooks/index.ts +1 -0
  85. package/src/features/company/hooks/useSubscriptionStatus.ts +71 -0
  86. package/src/features/user/components/forms/UserEditor.tsx +1 -1
  87. package/src/features/user/components/lists/AdminUsersList.tsx +1 -1
  88. package/src/features/user/contexts/CurrentUserContext.tsx +1 -1
  89. package/src/features/user/data/user.ts +1 -1
  90. package/dist/chunk-IXI4GAKB.js.map +0 -1
  91. package/dist/chunk-ORFXBO7F.mjs.map +0 -1
  92. package/dist/chunk-PYASRX75.mjs.map +0 -1
  93. package/dist/chunk-TSEU4KZ2.js.map +0 -1
  94. package/src/features/billing/stripe-subscription/components/forms/SubscriptionEditor.tsx +0 -331
  95. package/src/features/billing/stripe-subscription/components/widgets/PricingCardsGrid.tsx +0 -110
  96. /package/dist/{BlockNoteEditor-EOA4OEVX.mjs.map → BlockNoteEditor-F5KCNLVF.mjs.map} +0 -0
@@ -1,4 +1,7 @@
1
+ export * from "./IntervalToggle";
1
2
  export * from "./PricingCard";
2
- export * from "./PricingCardsGrid";
3
+ export * from "./ProductPricingList";
4
+ export * from "./ProductPricingRow";
3
5
  export * from "./ProrationPreview";
4
6
  export * from "./SubscriptionStatusBadge";
7
+ export * from "./SubscriptionConfirmation";
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "../../../../../shadcnui";
11
+ import { StripeSubscriptionInterface } from "../../data";
12
+ import { useSubscriptionWizard } from "../../hooks/useSubscriptionWizard";
13
+ import { WizardProgressIndicator } from "./WizardProgressIndicator";
14
+ import { WizardStepPlanSelection } from "./WizardStepPlanSelection";
15
+ import { WizardStepReview } from "./WizardStepReview";
16
+ import { WizardStepPaymentMethod } from "./WizardStepPaymentMethod";
17
+
18
+ export type SubscriptionWizardProps = {
19
+ open: boolean;
20
+ onOpenChange: (open: boolean) => void;
21
+ onSuccess: () => void;
22
+ hasActiveRecurringSubscription: boolean;
23
+ subscription?: StripeSubscriptionInterface;
24
+ };
25
+
26
+ export function SubscriptionWizard({
27
+ open,
28
+ onOpenChange,
29
+ onSuccess,
30
+ hasActiveRecurringSubscription,
31
+ subscription,
32
+ }: SubscriptionWizardProps) {
33
+ const handleClose = useCallback(() => onOpenChange(false), [onOpenChange]);
34
+
35
+ const { state, actions } = useSubscriptionWizard({
36
+ subscription,
37
+ onSuccess,
38
+ onClose: handleClose,
39
+ });
40
+
41
+ // Track if we've already checked payment method for this open state
42
+ const hasCheckedRef = useRef(false);
43
+
44
+ // Check payment method on mount
45
+ useEffect(() => {
46
+ if (open && !hasCheckedRef.current) {
47
+ hasCheckedRef.current = true;
48
+ actions.checkPaymentMethod();
49
+ }
50
+ if (!open) {
51
+ hasCheckedRef.current = false;
52
+ }
53
+ }, [open, actions.checkPaymentMethod]);
54
+
55
+ // Reset state when dialog closes
56
+ useEffect(() => {
57
+ if (!open) {
58
+ actions.reset();
59
+ }
60
+ }, [open, actions.reset]);
61
+
62
+ const dialogTitle = subscription ? "Change Subscription Plan" : "Subscribe to a Plan";
63
+ const dialogDescription = subscription
64
+ ? "Select a new plan for your subscription"
65
+ : "Choose a subscription plan to get started";
66
+
67
+ return (
68
+ <Dialog open={open} onOpenChange={onOpenChange}>
69
+ <DialogContent className="max-w-2xl">
70
+ <DialogHeader>
71
+ <DialogTitle>{dialogTitle}</DialogTitle>
72
+ <DialogDescription>{dialogDescription}</DialogDescription>
73
+ </DialogHeader>
74
+
75
+ <WizardProgressIndicator currentStep={state.step} />
76
+
77
+ {state.step === "plan-selection" && (
78
+ <WizardStepPlanSelection
79
+ selectedPrice={state.selectedPrice}
80
+ selectedInterval={state.selectedInterval}
81
+ currentPriceId={subscription?.price?.id}
82
+ hideRecurringPrices={hasActiveRecurringSubscription && !subscription}
83
+ onSelectPrice={actions.selectPrice}
84
+ onIntervalChange={actions.setInterval}
85
+ onNext={actions.goToReview}
86
+ isProcessing={state.isProcessing}
87
+ />
88
+ )}
89
+
90
+ {state.step === "review" && (
91
+ <WizardStepReview
92
+ selectedPrice={state.selectedPrice}
93
+ subscription={subscription}
94
+ prorationPreview={state.prorationPreview}
95
+ hasPaymentMethod={state.hasPaymentMethod}
96
+ error={state.error}
97
+ isProcessing={state.isProcessing}
98
+ onBack={() => actions.goToStep("plan-selection")}
99
+ onAddPaymentMethod={() => actions.goToStep("payment-method")}
100
+ onConfirm={actions.confirmSubscription}
101
+ />
102
+ )}
103
+
104
+ {state.step === "payment-method" && (
105
+ <WizardStepPaymentMethod
106
+ onBack={() => actions.goToStep("review")}
107
+ onSuccess={actions.handlePaymentMethodSuccess}
108
+ isProcessing={state.isProcessing}
109
+ />
110
+ )}
111
+ </DialogContent>
112
+ </Dialog>
113
+ );
114
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { Check } from "lucide-react";
4
+ import { WizardStep } from "../../hooks/useSubscriptionWizard";
5
+
6
+ type WizardProgressIndicatorProps = {
7
+ currentStep: WizardStep;
8
+ };
9
+
10
+ const STEPS: { key: WizardStep; label: string }[] = [
11
+ { key: "plan-selection", label: "Select Plan" },
12
+ { key: "review", label: "Review" },
13
+ { key: "payment-method", label: "Payment" },
14
+ ];
15
+
16
+ function getStepIndex(step: WizardStep): number {
17
+ return STEPS.findIndex((s) => s.key === step);
18
+ }
19
+
20
+ export function WizardProgressIndicator({ currentStep }: WizardProgressIndicatorProps) {
21
+ const currentIndex = getStepIndex(currentStep);
22
+
23
+ return (
24
+ <div className="flex items-center justify-center gap-x-2 py-4">
25
+ {STEPS.map((step, index) => {
26
+ const isCompleted = index < currentIndex;
27
+ const isCurrent = index === currentIndex;
28
+
29
+ return (
30
+ <div key={step.key} className="flex items-center gap-x-2">
31
+ {/* Step Indicator */}
32
+ <div
33
+ className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${
34
+ isCompleted
35
+ ? "bg-primary text-primary-foreground"
36
+ : isCurrent
37
+ ? "bg-primary text-primary-foreground"
38
+ : "bg-muted text-muted-foreground"
39
+ }`}
40
+ >
41
+ {isCompleted ? <Check className="h-4 w-4" /> : index + 1}
42
+ </div>
43
+
44
+ {/* Step Label */}
45
+ <span
46
+ className={`text-sm ${
47
+ isCurrent ? "font-medium text-foreground" : "text-muted-foreground"
48
+ }`}
49
+ >
50
+ {step.label}
51
+ </span>
52
+
53
+ {/* Connector */}
54
+ {index < STEPS.length - 1 && (
55
+ <div
56
+ className={`h-0.5 w-8 ${
57
+ index < currentIndex ? "bg-primary" : "bg-muted"
58
+ }`}
59
+ />
60
+ )}
61
+ </div>
62
+ );
63
+ })}
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { PaymentMethodForm } from "../../../stripe-customer/components/forms/PaymentMethodForm";
4
+
5
+ type WizardStepPaymentMethodProps = {
6
+ onBack: () => void;
7
+ onSuccess: () => void;
8
+ isProcessing: boolean;
9
+ };
10
+
11
+ export function WizardStepPaymentMethod({
12
+ onBack,
13
+ onSuccess,
14
+ isProcessing,
15
+ }: WizardStepPaymentMethodProps) {
16
+ return (
17
+ <div className="space-y-6">
18
+ <div className="text-center">
19
+ <h3 className="font-semibold text-lg">Add Payment Method</h3>
20
+ <p className="text-sm text-muted-foreground">
21
+ Enter your card details to complete your subscription
22
+ </p>
23
+ </div>
24
+
25
+ <PaymentMethodForm
26
+ onSuccess={onSuccess}
27
+ onCancel={onBack}
28
+ isLoading={isProcessing}
29
+ />
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { Button } from "../../../../../shadcnui";
5
+ import { StripeProductInterface, StripeProductService } from "../../../stripe-product";
6
+ import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
7
+ import { BillingInterval, IntervalToggle } from "../widgets/IntervalToggle";
8
+ import { ProductPricingList } from "../widgets/ProductPricingList";
9
+
10
+ type WizardStepPlanSelectionProps = {
11
+ selectedPrice: StripePriceInterface | null;
12
+ selectedInterval: BillingInterval;
13
+ currentPriceId?: string;
14
+ hideRecurringPrices: boolean;
15
+ onSelectPrice: (price: StripePriceInterface) => void;
16
+ onIntervalChange: (interval: BillingInterval) => void;
17
+ onNext: () => void;
18
+ isProcessing: boolean;
19
+ };
20
+
21
+ export function WizardStepPlanSelection({
22
+ selectedPrice,
23
+ selectedInterval,
24
+ currentPriceId,
25
+ hideRecurringPrices,
26
+ onSelectPrice,
27
+ onIntervalChange,
28
+ onNext,
29
+ isProcessing,
30
+ }: WizardStepPlanSelectionProps) {
31
+ const [products, setProducts] = useState<StripeProductInterface[]>([]);
32
+ const [loading, setLoading] = useState(true);
33
+
34
+ useEffect(() => {
35
+ const loadProducts = async () => {
36
+ try {
37
+ const fetchedProducts = await StripeProductService.listProducts({ active: true });
38
+ setProducts(fetchedProducts);
39
+ } catch (error) {
40
+ console.error("[WizardStepPlanSelection] Failed to load products:", error);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ };
45
+
46
+ loadProducts();
47
+ }, []);
48
+
49
+ // Compute available intervals from products
50
+ const { hasMonthly, hasYearly } = useMemo(() => {
51
+ let hasMonthly = false;
52
+ let hasYearly = false;
53
+
54
+ for (const product of products) {
55
+ for (const price of product.stripePrices || []) {
56
+ if (price.priceType === "recurring" && price.recurring?.interval === "month") {
57
+ hasMonthly = true;
58
+ }
59
+ if (price.priceType === "recurring" && price.recurring?.interval === "year") {
60
+ hasYearly = true;
61
+ }
62
+ }
63
+ }
64
+
65
+ return { hasMonthly, hasYearly };
66
+ }, [products]);
67
+
68
+ const handleSelectPrice = (price: StripePriceInterface) => {
69
+ onSelectPrice(price);
70
+ };
71
+
72
+ return (
73
+ <div className="space-y-6">
74
+ {/* Interval Toggle */}
75
+ <div className="flex justify-center">
76
+ <IntervalToggle
77
+ value={selectedInterval}
78
+ onChange={onIntervalChange}
79
+ hasMonthly={hasMonthly}
80
+ hasYearly={hasYearly}
81
+ />
82
+ </div>
83
+
84
+ {/* Product Pricing List */}
85
+ <ProductPricingList
86
+ products={products}
87
+ selectedInterval={selectedInterval}
88
+ currentPriceId={currentPriceId}
89
+ selectedPriceId={selectedPrice?.id}
90
+ loading={loading}
91
+ hideRecurringPrices={hideRecurringPrices}
92
+ onSelectPrice={handleSelectPrice}
93
+ />
94
+
95
+ {/* Action Buttons */}
96
+ <div className="flex justify-end pt-4 border-t">
97
+ <Button onClick={onNext} disabled={!selectedPrice || isProcessing}>
98
+ {isProcessing ? "Loading..." : "Next: Review"}
99
+ </Button>
100
+ </div>
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ import { AlertCircle } from "lucide-react";
4
+ import { Alert, AlertDescription, Button } from "../../../../../shadcnui";
5
+ import { formatCurrency } from "../../../components/utils/currency";
6
+ import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
7
+ import { ProrationPreviewInterface } from "../../../stripe-invoice/data/stripe-invoice.interface";
8
+ import { StripeSubscriptionInterface } from "../../data";
9
+
10
+ type WizardStepReviewProps = {
11
+ selectedPrice: StripePriceInterface | null;
12
+ subscription?: StripeSubscriptionInterface;
13
+ prorationPreview: ProrationPreviewInterface | null;
14
+ hasPaymentMethod: boolean;
15
+ error: string | null;
16
+ isProcessing: boolean;
17
+ onBack: () => void;
18
+ onAddPaymentMethod: () => void;
19
+ onConfirm: () => void;
20
+ };
21
+
22
+ export function WizardStepReview({
23
+ selectedPrice,
24
+ subscription,
25
+ prorationPreview,
26
+ hasPaymentMethod,
27
+ error,
28
+ isProcessing,
29
+ onBack,
30
+ onAddPaymentMethod,
31
+ onConfirm,
32
+ }: WizardStepReviewProps) {
33
+ if (!selectedPrice) {
34
+ return (
35
+ <div className="text-center py-8 text-muted-foreground">
36
+ No plan selected. Please go back and select a plan.
37
+ </div>
38
+ );
39
+ }
40
+
41
+ const isChangingPlan = subscription && subscription.price?.id !== selectedPrice.id;
42
+
43
+ const formatInterval = (price: StripePriceInterface) => {
44
+ if (price.priceType === "one_time") return "one-time";
45
+ const interval = price.recurring?.interval || "month";
46
+ return interval === "year" ? "yearly" : "monthly";
47
+ };
48
+
49
+ return (
50
+ <div className="space-y-6">
51
+ {/* Selected Plan Summary */}
52
+ <div className="bg-muted/50 rounded-lg p-4 space-y-3">
53
+ <h3 className="font-semibold text-lg">Selected Plan</h3>
54
+ <div className="flex justify-between items-center">
55
+ <div>
56
+ <p className="font-medium">{selectedPrice.product?.name}</p>
57
+ {selectedPrice.nickname && (
58
+ <p className="text-sm text-muted-foreground">{selectedPrice.nickname}</p>
59
+ )}
60
+ </div>
61
+ <div className="text-right">
62
+ <p className="font-semibold text-lg">
63
+ {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency)}
64
+ </p>
65
+ <p className="text-sm text-muted-foreground">{formatInterval(selectedPrice)}</p>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ {/* Proration Preview (for plan changes) */}
71
+ {isChangingPlan && prorationPreview && (
72
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-2">
73
+ <h4 className="font-medium text-blue-800">Proration Summary</h4>
74
+ <p className="text-sm text-blue-700">
75
+ Your next charge will be adjusted to account for the plan change.
76
+ </p>
77
+ <div className="flex justify-between text-sm">
78
+ <span className="text-blue-600">Amount due now:</span>
79
+ <span className="font-medium text-blue-800">
80
+ {formatCurrency(prorationPreview.amountDue, prorationPreview.currency)}
81
+ </span>
82
+ </div>
83
+ </div>
84
+ )}
85
+
86
+ {/* Payment Method Status */}
87
+ <div className="border rounded-lg p-4">
88
+ <div className="flex justify-between items-center">
89
+ <div>
90
+ <h4 className="font-medium">Payment Method</h4>
91
+ <p className="text-sm text-muted-foreground">
92
+ {hasPaymentMethod
93
+ ? "A payment method is on file"
94
+ : "No payment method on file"}
95
+ </p>
96
+ </div>
97
+ {!hasPaymentMethod && (
98
+ <Button variant="outline" onClick={onAddPaymentMethod}>
99
+ Add Payment Method
100
+ </Button>
101
+ )}
102
+ </div>
103
+ </div>
104
+
105
+ {/* Error Display */}
106
+ {error && (
107
+ <Alert variant="destructive">
108
+ <AlertCircle className="h-4 w-4" />
109
+ <AlertDescription>{error}</AlertDescription>
110
+ </Alert>
111
+ )}
112
+
113
+ {/* Action Buttons */}
114
+ <div className="flex justify-between pt-4 border-t">
115
+ <Button variant="outline" onClick={onBack} disabled={isProcessing}>
116
+ Back
117
+ </Button>
118
+ <Button
119
+ onClick={hasPaymentMethod ? onConfirm : onAddPaymentMethod}
120
+ disabled={isProcessing}
121
+ >
122
+ {isProcessing
123
+ ? "Processing..."
124
+ : hasPaymentMethod
125
+ ? isChangingPlan
126
+ ? "Confirm Plan Change"
127
+ : "Subscribe Now"
128
+ : "Add Payment Method"}
129
+ </Button>
130
+ </div>
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,6 @@
1
+ export { SubscriptionWizard } from "./SubscriptionWizard";
2
+ export type { SubscriptionWizardProps } from "./SubscriptionWizard";
3
+ export { WizardProgressIndicator } from "./WizardProgressIndicator";
4
+ export { WizardStepPlanSelection } from "./WizardStepPlanSelection";
5
+ export { WizardStepReview } from "./WizardStepReview";
6
+ export { WizardStepPaymentMethod } from "./WizardStepPaymentMethod";
@@ -0,0 +1,217 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useReducer, useRef } from "react";
4
+ import { StripeSubscriptionInterface, StripeSubscriptionService } from "../data";
5
+ import { StripePriceInterface } from "../../stripe-price/data/stripe-price.interface";
6
+ import { BillingInterval } from "../components/widgets/IntervalToggle";
7
+ import { StripeCustomerService } from "../../stripe-customer/data/stripe-customer.service";
8
+ import { ProrationPreviewInterface } from "../../stripe-invoice/data/stripe-invoice.interface";
9
+
10
+ export type WizardStep = "plan-selection" | "review" | "payment-method";
11
+
12
+ export type WizardState = {
13
+ step: WizardStep;
14
+ selectedPrice: StripePriceInterface | null;
15
+ selectedInterval: BillingInterval;
16
+ hasPaymentMethod: boolean;
17
+ isProcessing: boolean;
18
+ error: string | null;
19
+ prorationPreview: ProrationPreviewInterface | null;
20
+ };
21
+
22
+ type WizardAction =
23
+ | { type: "SET_STEP"; step: WizardStep }
24
+ | { type: "SELECT_PRICE"; price: StripePriceInterface }
25
+ | { type: "SET_INTERVAL"; interval: BillingInterval }
26
+ | { type: "SET_HAS_PAYMENT_METHOD"; hasMethod: boolean }
27
+ | { type: "SET_PROCESSING"; isProcessing: boolean }
28
+ | { type: "SET_ERROR"; error: string | null }
29
+ | { type: "SET_PRORATION_PREVIEW"; preview: ProrationPreviewInterface | null }
30
+ | { type: "RESET" };
31
+
32
+ const initialState: WizardState = {
33
+ step: "plan-selection",
34
+ selectedPrice: null,
35
+ selectedInterval: "month",
36
+ hasPaymentMethod: false,
37
+ isProcessing: false,
38
+ error: null,
39
+ prorationPreview: null,
40
+ };
41
+
42
+ function wizardReducer(state: WizardState, action: WizardAction): WizardState {
43
+ switch (action.type) {
44
+ case "SET_STEP":
45
+ return { ...state, step: action.step, error: null };
46
+ case "SELECT_PRICE":
47
+ return { ...state, selectedPrice: action.price };
48
+ case "SET_INTERVAL":
49
+ return { ...state, selectedInterval: action.interval };
50
+ case "SET_HAS_PAYMENT_METHOD":
51
+ return { ...state, hasPaymentMethod: action.hasMethod };
52
+ case "SET_PROCESSING":
53
+ return { ...state, isProcessing: action.isProcessing };
54
+ case "SET_ERROR":
55
+ return { ...state, error: action.error };
56
+ case "SET_PRORATION_PREVIEW":
57
+ return { ...state, prorationPreview: action.preview };
58
+ case "RESET":
59
+ return initialState;
60
+ default:
61
+ return state;
62
+ }
63
+ }
64
+
65
+ export type UseSubscriptionWizardOptions = {
66
+ subscription?: StripeSubscriptionInterface;
67
+ onSuccess: () => void;
68
+ onClose: () => void;
69
+ };
70
+
71
+ export function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseSubscriptionWizardOptions) {
72
+ const [state, dispatch] = useReducer(wizardReducer, {
73
+ ...initialState,
74
+ selectedPrice: subscription?.price || null,
75
+ });
76
+
77
+ // Use refs for callbacks to avoid dependency changes
78
+ const onSuccessRef = useRef(onSuccess);
79
+ const onCloseRef = useRef(onClose);
80
+ onSuccessRef.current = onSuccess;
81
+ onCloseRef.current = onClose;
82
+
83
+ const checkPaymentMethod = useCallback(async () => {
84
+ try {
85
+ const methods = await StripeCustomerService.listPaymentMethods();
86
+ dispatch({ type: "SET_HAS_PAYMENT_METHOD", hasMethod: methods.length > 0 });
87
+ } catch (error) {
88
+ console.error("[useSubscriptionWizard] Failed to check payment methods:", error);
89
+ dispatch({ type: "SET_HAS_PAYMENT_METHOD", hasMethod: false });
90
+ }
91
+ }, []);
92
+
93
+ const selectPrice = useCallback((price: StripePriceInterface) => {
94
+ dispatch({ type: "SELECT_PRICE", price });
95
+ }, []);
96
+
97
+ const setInterval = useCallback((interval: BillingInterval) => {
98
+ dispatch({ type: "SET_INTERVAL", interval });
99
+ }, []);
100
+
101
+ const goToStep = useCallback((step: WizardStep) => {
102
+ dispatch({ type: "SET_STEP", step });
103
+ }, []);
104
+
105
+ const goToReview = useCallback(async () => {
106
+ if (!state.selectedPrice) return;
107
+
108
+ dispatch({ type: "SET_PROCESSING", isProcessing: true });
109
+
110
+ try {
111
+ // Check payment method first
112
+ await checkPaymentMethod();
113
+
114
+ // If editing subscription, get proration preview
115
+ if (subscription && state.selectedPrice.id !== subscription.price?.id) {
116
+ const preview = await StripeSubscriptionService.getProrationPreview({
117
+ subscriptionId: subscription.id,
118
+ newPriceId: state.selectedPrice.id,
119
+ });
120
+ dispatch({ type: "SET_PRORATION_PREVIEW", preview });
121
+ }
122
+
123
+ dispatch({ type: "SET_STEP", step: "review" });
124
+ } catch (error: any) {
125
+ console.error("[useSubscriptionWizard] Error preparing review:", error);
126
+ dispatch({ type: "SET_ERROR", error: error?.message || "Failed to prepare review" });
127
+ } finally {
128
+ dispatch({ type: "SET_PROCESSING", isProcessing: false });
129
+ }
130
+ }, [state.selectedPrice, subscription, checkPaymentMethod]);
131
+
132
+ const confirmSubscription = useCallback(async () => {
133
+ if (!state.selectedPrice) return;
134
+
135
+ dispatch({ type: "SET_PROCESSING", isProcessing: true });
136
+ dispatch({ type: "SET_ERROR", error: null });
137
+
138
+ try {
139
+ if (subscription) {
140
+ // Change existing subscription
141
+ await StripeSubscriptionService.changePlan({
142
+ id: subscription.id,
143
+ newPriceId: state.selectedPrice.id,
144
+ });
145
+ } else {
146
+ // Create new subscription
147
+ await StripeSubscriptionService.createSubscription({
148
+ id: crypto.randomUUID(),
149
+ priceId: state.selectedPrice.id,
150
+ });
151
+ }
152
+
153
+ onSuccessRef.current();
154
+ onCloseRef.current();
155
+ } catch (error: any) {
156
+ console.error("[useSubscriptionWizard] Subscription error:", error);
157
+
158
+ // Handle 409 Conflict - duplicate recurring subscription
159
+ if (error?.status === 409 || error?.response?.status === 409) {
160
+ dispatch({
161
+ type: "SET_ERROR",
162
+ error: "You already have an active subscription. Please change your existing plan instead.",
163
+ });
164
+ return;
165
+ }
166
+
167
+ // Handle 402 - payment method required
168
+ if (error?.status === 402 || error?.response?.status === 402) {
169
+ dispatch({ type: "SET_HAS_PAYMENT_METHOD", hasMethod: false });
170
+ dispatch({ type: "SET_STEP", step: "payment-method" });
171
+ return;
172
+ }
173
+
174
+ dispatch({ type: "SET_ERROR", error: error?.message || "Failed to process subscription" });
175
+ } finally {
176
+ dispatch({ type: "SET_PROCESSING", isProcessing: false });
177
+ }
178
+ }, [state.selectedPrice, subscription]);
179
+
180
+ const handlePaymentMethodSuccess = useCallback(async () => {
181
+ dispatch({ type: "SET_HAS_PAYMENT_METHOD", hasMethod: true });
182
+ // Go back to review to confirm subscription
183
+ dispatch({ type: "SET_STEP", step: "review" });
184
+ }, []);
185
+
186
+ const reset = useCallback(() => {
187
+ dispatch({ type: "RESET" });
188
+ }, []);
189
+
190
+ const actions = useMemo(
191
+ () => ({
192
+ selectPrice,
193
+ setInterval,
194
+ goToStep,
195
+ goToReview,
196
+ confirmSubscription,
197
+ handlePaymentMethodSuccess,
198
+ checkPaymentMethod,
199
+ reset,
200
+ }),
201
+ [
202
+ selectPrice,
203
+ setInterval,
204
+ goToStep,
205
+ goToReview,
206
+ confirmSubscription,
207
+ handlePaymentMethodSuccess,
208
+ checkPaymentMethod,
209
+ reset,
210
+ ],
211
+ );
212
+
213
+ return {
214
+ state,
215
+ actions,
216
+ };
217
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./data";
2
2
  export * from "./stripe-subscription.module";
3
3
 
4
- // Note: hooks are not exported from barrel to avoid bundling client code into server components.
5
- // Import hooks directly: import { useConfirmSubscriptionPayment } from "./hooks";
4
+ // Note: Client components (wizards, hooks) are not exported here to avoid bundling
5
+ // client code into server contexts. Import them from the /billing entry point:
6
+ // import { SubscriptionWizard } from "@carlonicora/nextjs-jsonapi/billing";