@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.
- package/dist/{BlockNoteEditor-CAUNVZUF.js → BlockNoteEditor-YBVEOPV4.js} +13 -13
- package/dist/{BlockNoteEditor-CAUNVZUF.js.map → BlockNoteEditor-YBVEOPV4.js.map} +1 -1
- package/dist/{BlockNoteEditor-EOA4OEVX.mjs → BlockNoteEditor-ZM4YPXHO.mjs} +3 -3
- package/dist/billing/index.d.mts +47 -17
- package/dist/billing/index.d.ts +47 -17
- package/dist/billing/index.js +1241 -1073
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +1375 -1207
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-IXI4GAKB.js → chunk-3X7EEFMN.js} +488 -431
- package/dist/chunk-3X7EEFMN.js.map +1 -0
- package/dist/{chunk-ORFXBO7F.mjs → chunk-DU64WMZD.mjs} +6 -3
- package/dist/chunk-DU64WMZD.mjs.map +1 -0
- package/dist/{chunk-TSEU4KZ2.js → chunk-J22NEVSK.js} +21 -18
- package/dist/chunk-J22NEVSK.js.map +1 -0
- package/dist/{chunk-PYASRX75.mjs → chunk-UCD5CUE4.mjs} +81 -24
- package/dist/chunk-UCD5CUE4.mjs.map +1 -0
- package/dist/client/index.d.mts +14 -5
- package/dist/client/index.d.ts +14 -5
- package/dist/client/index.js +5 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +4 -2
- package/dist/components/index.d.mts +2 -2
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.js +3 -3
- package/dist/components/index.mjs +2 -2
- package/dist/{config-B4pZpLT9.d.ts → config-CHwoRDOp.d.ts} +1 -1
- package/dist/{config-DT1K-t6I.d.mts → config-DiWyJzk9.d.mts} +1 -1
- package/dist/{content.interface-B2Ldg0vg.d.mts → content.interface-BSpowEiW.d.mts} +1 -1
- package/dist/{content.interface-D8NHv3DX.d.ts → content.interface-DFQ7mkpL.d.ts} +1 -1
- package/dist/contexts/index.d.mts +2 -2
- package/dist/contexts/index.d.ts +2 -2
- package/dist/contexts/index.js +3 -3
- package/dist/contexts/index.mjs +2 -2
- package/dist/core/index.d.mts +39 -37
- package/dist/core/index.d.ts +39 -37
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{notification.interface-H0L9WBge.d.ts → notification.interface-CmKmObIU.d.ts} +1 -0
- package/dist/{notification.interface-DEn-Yp_b.d.mts → notification.interface-D5MbtfZK.d.mts} +1 -0
- package/dist/{s3.service-BNytYanU.d.mts → s3.service-BMT7W6KS.d.mts} +19 -19
- package/dist/{s3.service-C7f_Ygz5.d.ts → s3.service-DsXo9nop.d.ts} +19 -19
- package/dist/server/index.d.mts +3 -3
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/{useSocket-BcnThTD0.d.mts → useSocket-DUqGoPya.d.mts} +1 -1
- package/dist/{useSocket-QZTOCzRF.d.ts → useSocket-QuHa0ZmO.d.ts} +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +1 -0
- package/src/components/forms/FormSelect.tsx +2 -1
- package/src/features/auth/data/auth.ts +0 -2
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +60 -3
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +12 -152
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +168 -0
- package/src/features/billing/stripe-customer/components/forms/index.ts +1 -0
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +19 -1
- package/src/features/billing/stripe-product/components/forms/ProductEditor.tsx +2 -2
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -235
- package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +7 -18
- package/src/features/billing/stripe-subscription/components/forms/index.ts +0 -1
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +10 -1
- package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +28 -0
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +128 -0
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +54 -0
- package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +68 -0
- package/src/features/billing/stripe-subscription/components/widgets/index.ts +4 -1
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +114 -0
- package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +66 -0
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +32 -0
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +103 -0
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +133 -0
- package/src/features/billing/stripe-subscription/components/wizards/index.ts +6 -0
- package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +217 -0
- package/src/features/billing/stripe-subscription/index.ts +3 -2
- package/src/features/company/components/details/TokenStatusIndicator.tsx +19 -9
- package/src/features/company/data/company.interface.ts +2 -0
- package/src/features/company/data/company.ts +7 -0
- package/src/features/company/hooks/index.ts +1 -0
- package/src/features/company/hooks/useSubscriptionStatus.ts +71 -0
- package/src/features/user/components/forms/UserEditor.tsx +1 -1
- package/src/features/user/components/lists/AdminUsersList.tsx +1 -1
- package/src/features/user/contexts/CurrentUserContext.tsx +1 -1
- package/src/features/user/data/user.ts +1 -1
- package/dist/chunk-IXI4GAKB.js.map +0 -1
- package/dist/chunk-ORFXBO7F.mjs.map +0 -1
- package/dist/chunk-PYASRX75.mjs.map +0 -1
- package/dist/chunk-TSEU4KZ2.js.map +0 -1
- package/src/features/billing/stripe-subscription/components/forms/SubscriptionEditor.tsx +0 -331
- package/src/features/billing/stripe-subscription/components/widgets/PricingCardsGrid.tsx +0 -110
- /package/dist/{BlockNoteEditor-EOA4OEVX.mjs.map → BlockNoteEditor-ZM4YPXHO.mjs.map} +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
export * from "./IntervalToggle";
|
|
1
2
|
export * from "./PricingCard";
|
|
2
|
-
export * from "./
|
|
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
|
+
}
|
package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx
ADDED
|
@@ -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
|
|
5
|
-
//
|
|
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";
|