@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,331 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { CheckCircle, Loader2 } from "lucide-react";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
5
|
-
import { v4 } from "uuid";
|
|
6
|
-
import {
|
|
7
|
-
Alert,
|
|
8
|
-
AlertDescription,
|
|
9
|
-
AlertTitle,
|
|
10
|
-
Button,
|
|
11
|
-
Dialog,
|
|
12
|
-
DialogContent,
|
|
13
|
-
DialogDescription,
|
|
14
|
-
DialogHeader,
|
|
15
|
-
DialogTitle,
|
|
16
|
-
} from "../../../../../shadcnui";
|
|
17
|
-
import { StripeCustomerService } from "../../../stripe-customer/data/stripe-customer.service";
|
|
18
|
-
import { ProrationPreviewInterface } from "../../../stripe-invoice/data/stripe-invoice.interface";
|
|
19
|
-
import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
|
|
20
|
-
import { StripeProductInterface, StripeProductService } from "../../../stripe-product";
|
|
21
|
-
import { StripeSubscriptionInterface, StripeSubscriptionService } from "../../data";
|
|
22
|
-
import { useConfirmSubscriptionPayment } from "../../hooks";
|
|
23
|
-
import { PricesByProduct, PricingCardsGrid } from "../widgets/PricingCardsGrid";
|
|
24
|
-
import { ProrationPreview } from "../widgets/ProrationPreview";
|
|
25
|
-
|
|
26
|
-
type PaymentConfirmationState = "idle" | "confirming" | "success" | "error";
|
|
27
|
-
|
|
28
|
-
type SubscriptionEditorProps = {
|
|
29
|
-
subscription?: StripeSubscriptionInterface;
|
|
30
|
-
open: boolean;
|
|
31
|
-
onOpenChange: (open: boolean) => void;
|
|
32
|
-
onSuccess: () => void;
|
|
33
|
-
onAddPaymentMethod?: () => void;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export function SubscriptionEditor({
|
|
37
|
-
subscription,
|
|
38
|
-
open,
|
|
39
|
-
onOpenChange,
|
|
40
|
-
onSuccess,
|
|
41
|
-
onAddPaymentMethod,
|
|
42
|
-
}: SubscriptionEditorProps) {
|
|
43
|
-
const { confirmPayment, isConfirming } = useConfirmSubscriptionPayment();
|
|
44
|
-
|
|
45
|
-
const [products, setProducts] = useState<StripeProductInterface[]>([]);
|
|
46
|
-
const [pricesByProduct, setPricesByProduct] = useState<PricesByProduct>(new Map());
|
|
47
|
-
const [loading, setLoading] = useState<boolean>(true);
|
|
48
|
-
const [selectedPriceId, setSelectedPriceId] = useState<string | null>(null);
|
|
49
|
-
const [loadingPriceId, setLoadingPriceId] = useState<string | null>(null);
|
|
50
|
-
const [prorationPreview, setProrationPreview] = useState<ProrationPreviewInterface | null>(null);
|
|
51
|
-
const [loadingProration, setLoadingProration] = useState<boolean>(false);
|
|
52
|
-
const [hasPaymentMethod, setHasPaymentMethod] = useState<boolean>(true);
|
|
53
|
-
const [loadingPaymentMethods, setLoadingPaymentMethods] = useState<boolean>(true);
|
|
54
|
-
const [paymentRequiredError, setPaymentRequiredError] = useState<boolean>(false);
|
|
55
|
-
const [paymentConfirmationState, setPaymentConfirmationState] = useState<PaymentConfirmationState>("idle");
|
|
56
|
-
const [paymentError, setPaymentError] = useState<string | null>(null);
|
|
57
|
-
|
|
58
|
-
// Get current subscription price if editing (use internal UUID for comparison)
|
|
59
|
-
const currentPriceId = subscription?.price?.id;
|
|
60
|
-
const isEditMode = !!subscription;
|
|
61
|
-
|
|
62
|
-
// Check payment methods on mount (only for new subscriptions)
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
const checkPaymentMethods = async () => {
|
|
65
|
-
if (subscription) {
|
|
66
|
-
// Editing existing subscription doesn't need payment method check
|
|
67
|
-
setLoadingPaymentMethods(false);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
setLoadingPaymentMethods(true);
|
|
72
|
-
try {
|
|
73
|
-
const paymentMethods = await StripeCustomerService.listPaymentMethods();
|
|
74
|
-
const hasMethod = paymentMethods.length > 0;
|
|
75
|
-
setHasPaymentMethod(hasMethod);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.error("[SubscriptionEditor] Failed to check payment methods:", error);
|
|
78
|
-
setHasPaymentMethod(false);
|
|
79
|
-
} finally {
|
|
80
|
-
setLoadingPaymentMethods(false);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
if (open) {
|
|
85
|
-
checkPaymentMethods();
|
|
86
|
-
}
|
|
87
|
-
}, [open, subscription]);
|
|
88
|
-
|
|
89
|
-
// Load products with prices on mount
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
const loadData = async () => {
|
|
92
|
-
setLoading(true);
|
|
93
|
-
try {
|
|
94
|
-
const fetchedProducts = await StripeProductService.listProducts({ active: true });
|
|
95
|
-
|
|
96
|
-
// Build prices map from product.stripePrices
|
|
97
|
-
const grouped: PricesByProduct = new Map();
|
|
98
|
-
for (const product of fetchedProducts) {
|
|
99
|
-
if (product.stripePrices && product.stripePrices.length > 0) {
|
|
100
|
-
grouped.set(product.id, product.stripePrices);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
setProducts(fetchedProducts);
|
|
105
|
-
setPricesByProduct(grouped);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.error("[SubscriptionEditor] Failed to load products/prices:", error);
|
|
108
|
-
} finally {
|
|
109
|
-
setLoading(false);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
if (open) {
|
|
114
|
-
loadData();
|
|
115
|
-
}
|
|
116
|
-
}, [open]);
|
|
117
|
-
|
|
118
|
-
// Load proration preview when editing and price is selected
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
const loadProration = async () => {
|
|
121
|
-
if (!subscription || !selectedPriceId || selectedPriceId === currentPriceId) {
|
|
122
|
-
setProrationPreview(null);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
setLoadingProration(true);
|
|
127
|
-
try {
|
|
128
|
-
const preview = await StripeSubscriptionService.getProrationPreview({
|
|
129
|
-
subscriptionId: subscription.id,
|
|
130
|
-
newPriceId: selectedPriceId,
|
|
131
|
-
});
|
|
132
|
-
setProrationPreview(preview);
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error("[SubscriptionEditor] Failed to load proration preview:", error);
|
|
135
|
-
setProrationPreview(null);
|
|
136
|
-
} finally {
|
|
137
|
-
setLoadingProration(false);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
loadProration();
|
|
142
|
-
}, [selectedPriceId, subscription, currentPriceId]);
|
|
143
|
-
|
|
144
|
-
const handleSelectPrice = async (price: StripePriceInterface) => {
|
|
145
|
-
const priceId = price.id; // Use internal UUID, not Stripe ID
|
|
146
|
-
|
|
147
|
-
if (isEditMode) {
|
|
148
|
-
// Edit mode: just select the price to show proration preview
|
|
149
|
-
setSelectedPriceId(priceId);
|
|
150
|
-
} else {
|
|
151
|
-
// Create mode: immediately create subscription
|
|
152
|
-
setLoadingPriceId(priceId);
|
|
153
|
-
setSelectedPriceId(priceId);
|
|
154
|
-
setPaymentError(null);
|
|
155
|
-
setPaymentConfirmationState("idle");
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
const result = await StripeSubscriptionService.createSubscription({
|
|
159
|
-
id: v4(),
|
|
160
|
-
priceId,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Check if payment confirmation is required (SCA flow)
|
|
164
|
-
if (result.meta.requiresAction && result.meta.clientSecret) {
|
|
165
|
-
setPaymentConfirmationState("confirming");
|
|
166
|
-
|
|
167
|
-
const confirmation = await confirmPayment(result.meta.clientSecret);
|
|
168
|
-
|
|
169
|
-
if (!confirmation.success) {
|
|
170
|
-
console.error("[SubscriptionEditor] Payment confirmation failed:", confirmation.error);
|
|
171
|
-
setPaymentConfirmationState("error");
|
|
172
|
-
setPaymentError(confirmation.error || "Payment confirmation failed");
|
|
173
|
-
setLoadingPriceId(null);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Sync subscription to get updated status from Stripe
|
|
178
|
-
await StripeSubscriptionService.syncSubscription({
|
|
179
|
-
subscriptionId: result.subscription.id,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Success - show brief success state then close
|
|
184
|
-
setPaymentConfirmationState("success");
|
|
185
|
-
setTimeout(() => {
|
|
186
|
-
onSuccess();
|
|
187
|
-
onOpenChange(false);
|
|
188
|
-
}, 1000);
|
|
189
|
-
} catch (error: any) {
|
|
190
|
-
console.error("[SubscriptionEditor] Failed to create subscription:", error);
|
|
191
|
-
// Handle 402 Payment Required error
|
|
192
|
-
if (error?.status === 402 || error?.response?.status === 402) {
|
|
193
|
-
setPaymentRequiredError(true);
|
|
194
|
-
setHasPaymentMethod(false);
|
|
195
|
-
} else {
|
|
196
|
-
setPaymentConfirmationState("error");
|
|
197
|
-
setPaymentError(error?.message || "Failed to create subscription");
|
|
198
|
-
}
|
|
199
|
-
setLoadingPriceId(null);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const handleConfirmPlanChange = async () => {
|
|
205
|
-
if (!subscription || !selectedPriceId) return;
|
|
206
|
-
|
|
207
|
-
setLoadingPriceId(selectedPriceId);
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
await StripeSubscriptionService.changePlan({
|
|
211
|
-
id: subscription.id,
|
|
212
|
-
newPriceId: selectedPriceId,
|
|
213
|
-
});
|
|
214
|
-
onSuccess();
|
|
215
|
-
onOpenChange(false);
|
|
216
|
-
} catch (error) {
|
|
217
|
-
console.error("[SubscriptionEditor] Failed to change plan:", error);
|
|
218
|
-
} finally {
|
|
219
|
-
setLoadingPriceId(null);
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const handleCancel = () => {
|
|
224
|
-
setSelectedPriceId(null);
|
|
225
|
-
setProrationPreview(null);
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
return (
|
|
229
|
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
230
|
-
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
231
|
-
<DialogHeader>
|
|
232
|
-
<DialogTitle>{subscription ? "Change Plan" : "Subscribe to a Plan"}</DialogTitle>
|
|
233
|
-
<DialogDescription>
|
|
234
|
-
{subscription
|
|
235
|
-
? "Select a new plan to switch to. You'll see a proration preview before confirming."
|
|
236
|
-
: "Choose a plan to start your subscription."}
|
|
237
|
-
</DialogDescription>
|
|
238
|
-
</DialogHeader>
|
|
239
|
-
|
|
240
|
-
{loadingPaymentMethods && !subscription ? (
|
|
241
|
-
<div className="flex items-center justify-center py-8">
|
|
242
|
-
<div className="text-muted-foreground">Checking payment methods...</div>
|
|
243
|
-
</div>
|
|
244
|
-
) : !hasPaymentMethod && !subscription ? (
|
|
245
|
-
<Alert variant="destructive">
|
|
246
|
-
<AlertTitle>Payment Method Required</AlertTitle>
|
|
247
|
-
<AlertDescription className="mt-2">
|
|
248
|
-
<p className="mb-4">
|
|
249
|
-
{paymentRequiredError
|
|
250
|
-
? "Your subscription could not be created because no payment method is on file."
|
|
251
|
-
: "You need to add a payment method before you can subscribe to a plan."}
|
|
252
|
-
</p>
|
|
253
|
-
{onAddPaymentMethod && (
|
|
254
|
-
<Button onClick={onAddPaymentMethod} variant="outline">
|
|
255
|
-
Add Payment Method
|
|
256
|
-
</Button>
|
|
257
|
-
)}
|
|
258
|
-
</AlertDescription>
|
|
259
|
-
</Alert>
|
|
260
|
-
) : paymentConfirmationState === "confirming" || isConfirming ? (
|
|
261
|
-
<div className="flex flex-col items-center justify-center py-12 space-y-4">
|
|
262
|
-
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
263
|
-
<div className="text-center">
|
|
264
|
-
<p className="font-medium">Processing payment...</p>
|
|
265
|
-
<p className="text-sm text-muted-foreground">Please complete any verification if prompted.</p>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
) : paymentConfirmationState === "success" ? (
|
|
269
|
-
<div className="flex flex-col items-center justify-center py-12 space-y-4">
|
|
270
|
-
<CheckCircle className="h-12 w-12 text-green-500" />
|
|
271
|
-
<div className="text-center">
|
|
272
|
-
<p className="font-medium text-green-600">Payment successful!</p>
|
|
273
|
-
<p className="text-sm text-muted-foreground">Your subscription is now active.</p>
|
|
274
|
-
</div>
|
|
275
|
-
</div>
|
|
276
|
-
) : paymentConfirmationState === "error" ? (
|
|
277
|
-
<div className="space-y-4">
|
|
278
|
-
<Alert variant="destructive">
|
|
279
|
-
<AlertTitle>Payment Failed</AlertTitle>
|
|
280
|
-
<AlertDescription className="mt-2">
|
|
281
|
-
<p className="mb-4">{paymentError || "We couldn't process your payment. Please try again."}</p>
|
|
282
|
-
<Button
|
|
283
|
-
onClick={() => {
|
|
284
|
-
setPaymentConfirmationState("idle");
|
|
285
|
-
setPaymentError(null);
|
|
286
|
-
setLoadingPriceId(null);
|
|
287
|
-
}}
|
|
288
|
-
variant="outline"
|
|
289
|
-
>
|
|
290
|
-
Try Again
|
|
291
|
-
</Button>
|
|
292
|
-
</AlertDescription>
|
|
293
|
-
</Alert>
|
|
294
|
-
</div>
|
|
295
|
-
) : (
|
|
296
|
-
<div className="space-y-6">
|
|
297
|
-
<PricingCardsGrid
|
|
298
|
-
products={products}
|
|
299
|
-
pricesByProduct={pricesByProduct}
|
|
300
|
-
currentPriceId={currentPriceId}
|
|
301
|
-
selectedPriceId={selectedPriceId ?? undefined}
|
|
302
|
-
loadingPriceId={loadingPriceId ?? undefined}
|
|
303
|
-
loading={loading}
|
|
304
|
-
onSelectPrice={handleSelectPrice}
|
|
305
|
-
/>
|
|
306
|
-
|
|
307
|
-
{isEditMode && loadingProration && (
|
|
308
|
-
<div className="bg-muted/50 rounded-lg p-4 text-sm text-muted-foreground text-center">
|
|
309
|
-
Loading proration preview...
|
|
310
|
-
</div>
|
|
311
|
-
)}
|
|
312
|
-
|
|
313
|
-
{isEditMode && prorationPreview && !loadingProration && (
|
|
314
|
-
<div className="space-y-4">
|
|
315
|
-
<ProrationPreview preview={prorationPreview} />
|
|
316
|
-
<div className="flex justify-end gap-3">
|
|
317
|
-
<Button variant="outline" onClick={handleCancel} disabled={!!loadingPriceId}>
|
|
318
|
-
Cancel
|
|
319
|
-
</Button>
|
|
320
|
-
<Button onClick={handleConfirmPlanChange} disabled={!!loadingPriceId}>
|
|
321
|
-
{loadingPriceId ? "Processing..." : "Confirm Plan Change"}
|
|
322
|
-
</Button>
|
|
323
|
-
</div>
|
|
324
|
-
</div>
|
|
325
|
-
)}
|
|
326
|
-
</div>
|
|
327
|
-
)}
|
|
328
|
-
</DialogContent>
|
|
329
|
-
</Dialog>
|
|
330
|
-
);
|
|
331
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Card, CardContent, CardFooter, CardHeader, Skeleton } from "../../../../../shadcnui";
|
|
4
|
-
import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
|
|
5
|
-
import { StripeProductInterface } from "../../../stripe-product";
|
|
6
|
-
import { PricingCard } from "./PricingCard";
|
|
7
|
-
|
|
8
|
-
export type PricesByProduct = Map<string, StripePriceInterface[]>;
|
|
9
|
-
|
|
10
|
-
export type PricingCardsGridProps = {
|
|
11
|
-
products: StripeProductInterface[];
|
|
12
|
-
pricesByProduct: PricesByProduct;
|
|
13
|
-
currentPriceId?: string;
|
|
14
|
-
selectedPriceId?: string;
|
|
15
|
-
loadingPriceId?: string;
|
|
16
|
-
loading?: boolean;
|
|
17
|
-
onSelectPrice: (price: StripePriceInterface) => void;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function PricingCardsGrid({
|
|
21
|
-
products,
|
|
22
|
-
pricesByProduct,
|
|
23
|
-
currentPriceId,
|
|
24
|
-
selectedPriceId,
|
|
25
|
-
loadingPriceId,
|
|
26
|
-
loading = false,
|
|
27
|
-
onSelectPrice,
|
|
28
|
-
}: PricingCardsGridProps) {
|
|
29
|
-
if (loading) {
|
|
30
|
-
return <PricingCardsGridSkeleton />;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (products.length === 0) {
|
|
34
|
-
return (
|
|
35
|
-
<div className="text-center py-8 text-muted-foreground">
|
|
36
|
-
No plans available
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className="space-y-8" role="radiogroup" aria-label="Available pricing plans">
|
|
43
|
-
{products.map((product) => {
|
|
44
|
-
const prices = pricesByProduct.get(product.id) || [];
|
|
45
|
-
if (prices.length === 0) return null;
|
|
46
|
-
|
|
47
|
-
// Sort prices from cheapest to most expensive
|
|
48
|
-
const sortedPrices = [...prices].sort((a, b) => (a.unitAmount ?? 0) - (b.unitAmount ?? 0));
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div key={product.id} className="space-y-4">
|
|
52
|
-
<h3 className="text-lg font-semibold">{product.name}</h3>
|
|
53
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
54
|
-
{sortedPrices.map((price) => (
|
|
55
|
-
<PricingCard
|
|
56
|
-
key={price.stripePriceId}
|
|
57
|
-
price={price}
|
|
58
|
-
isCurrentPlan={price.stripePriceId === currentPriceId}
|
|
59
|
-
isSelected={price.stripePriceId === selectedPriceId}
|
|
60
|
-
isLoading={price.stripePriceId === loadingPriceId}
|
|
61
|
-
onSelect={onSelectPrice}
|
|
62
|
-
/>
|
|
63
|
-
))}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
})}
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function PricingCardsGridSkeleton() {
|
|
73
|
-
return (
|
|
74
|
-
<div className="space-y-8">
|
|
75
|
-
{[1, 2].map((productIndex) => (
|
|
76
|
-
<div key={productIndex} className="space-y-4">
|
|
77
|
-
<Skeleton className="h-6 w-32" />
|
|
78
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
79
|
-
{[1, 2, 3].map((cardIndex) => (
|
|
80
|
-
<Card key={cardIndex} className="animate-pulse">
|
|
81
|
-
<CardHeader className="pb-2">
|
|
82
|
-
<Skeleton className="h-5 w-24" />
|
|
83
|
-
</CardHeader>
|
|
84
|
-
<CardContent className="pb-4">
|
|
85
|
-
<div className="mb-4">
|
|
86
|
-
<Skeleton className="h-9 w-20 inline-block" />
|
|
87
|
-
<Skeleton className="h-4 w-12 inline-block ml-2" />
|
|
88
|
-
</div>
|
|
89
|
-
<div className="space-y-2">
|
|
90
|
-
<div className="flex items-center gap-2">
|
|
91
|
-
<Skeleton className="h-4 w-4 rounded-full" />
|
|
92
|
-
<Skeleton className="h-4 w-32" />
|
|
93
|
-
</div>
|
|
94
|
-
<div className="flex items-center gap-2">
|
|
95
|
-
<Skeleton className="h-4 w-4 rounded-full" />
|
|
96
|
-
<Skeleton className="h-4 w-28" />
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
</CardContent>
|
|
100
|
-
<CardFooter>
|
|
101
|
-
<Skeleton className="h-9 w-full" />
|
|
102
|
-
</CardFooter>
|
|
103
|
-
</Card>
|
|
104
|
-
))}
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
))}
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
File without changes
|