@omnikit-js/ui 0.5.4 → 0.6.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/README.md +100 -255
- package/dist/components/client/index.d.mts +7 -0
- package/dist/components/client/index.mjs +6 -0
- package/dist/components/client/index.mjs.map +1 -0
- package/dist/components/server/index.d.mts +2 -0
- package/dist/components/server/index.mjs +201 -0
- package/dist/components/server/index.mjs.map +1 -0
- package/dist/index-D-5etDTV.d.mts +80 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.mjs +204 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +41 -77
- package/LICENSE +0 -21
- package/dist/styles.css +0 -2486
- package/dist/styles.isolated.css +0 -2643
- package/dist/styles.prefixed.css +0 -248
- package/src/components/BillingClient.tsx +0 -686
- package/src/components/BillingServer.tsx +0 -129
- package/src/components/PaymentMethodForm.tsx +0 -125
- package/src/components/SubscriptionConfirmModal.tsx +0 -213
- package/src/components/theme-provider.tsx +0 -11
- package/src/components/ui/alert.tsx +0 -59
- package/src/components/ui/avatar.tsx +0 -53
- package/src/components/ui/badge.tsx +0 -46
- package/src/components/ui/button.tsx +0 -59
- package/src/components/ui/card-brand-icon.tsx +0 -88
- package/src/components/ui/card.tsx +0 -92
- package/src/components/ui/chart.tsx +0 -353
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -257
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/navigation-menu.tsx +0 -168
- package/src/components/ui/progress.tsx +0 -31
- package/src/components/ui/radio-group.tsx +0 -44
- package/src/components/ui/select.tsx +0 -185
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/switch.tsx +0 -31
- package/src/components/ui/tabs.tsx +0 -66
- package/src/components/ui/textarea.tsx +0 -18
- package/src/components/ui/tooltip.tsx +0 -30
- package/src/index.ts +0 -13
- package/src/lib/utils.ts +0 -6
- package/src/styles/globals.css +0 -1
- package/src/styles/isolated.css +0 -316
- package/src/styles/main.css +0 -95
- package/src/styles/prefixed-main.css +0 -95
- package/src/styles/prefixed.css +0 -1
|
@@ -1,686 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
5
|
-
import { loadStripe } from '@stripe/stripe-js';
|
|
6
|
-
import { Elements } from '@stripe/react-stripe-js';
|
|
7
|
-
import { Button } from "./ui/button";
|
|
8
|
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
|
9
|
-
import { Badge } from "./ui/badge";
|
|
10
|
-
import { Progress } from "./ui/progress";
|
|
11
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
|
12
|
-
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
|
|
13
|
-
import { Check, CreditCard, FileText, AlertCircle, AlertTriangle, CheckCircle, Info, Loader2 } from "lucide-react";
|
|
14
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
15
|
-
import PaymentMethodForm from './PaymentMethodForm';
|
|
16
|
-
import SubscriptionConfirmModal from './SubscriptionConfirmModal';
|
|
17
|
-
import { CardBrandIcon } from './ui/card-brand-icon';
|
|
18
|
-
|
|
19
|
-
interface BillingClientProps {
|
|
20
|
-
customerData: any;
|
|
21
|
-
pricingData: any;
|
|
22
|
-
paymentMethods: any;
|
|
23
|
-
apiUrl: string;
|
|
24
|
-
apiKey: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '');
|
|
28
|
-
|
|
29
|
-
// Client-side actions
|
|
30
|
-
async function cancelSubscription(subscriptionId: string, apiUrl: string, apiKey: string) {
|
|
31
|
-
const response = await fetch(`${apiUrl}/billing/subscriptions/${subscriptionId}`, {
|
|
32
|
-
method: 'DELETE',
|
|
33
|
-
headers: {
|
|
34
|
-
'x-api-key': apiKey,
|
|
35
|
-
'Content-Type': 'application/json'
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
if (!response.ok) {
|
|
40
|
-
throw new Error('Failed to cancel subscription');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return await response.json();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function setDefaultPaymentMethod(paymentMethodId: string, apiUrl: string, apiKey: string) {
|
|
47
|
-
const response = await fetch(`${apiUrl}/billing/payment-methods/${paymentMethodId}/default`, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: {
|
|
50
|
-
'x-api-key': apiKey,
|
|
51
|
-
'Content-Type': 'application/json'
|
|
52
|
-
},
|
|
53
|
-
body: JSON.stringify({})
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
throw new Error('Failed to set default payment method');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return await response.json();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function deletePaymentMethod(paymentMethodId: string, apiUrl: string, apiKey: string) {
|
|
64
|
-
const response = await fetch(`${apiUrl}/billing/payment-methods/${paymentMethodId}`, {
|
|
65
|
-
method: 'DELETE',
|
|
66
|
-
headers: {
|
|
67
|
-
'x-api-key': apiKey,
|
|
68
|
-
'Content-Type': 'application/json'
|
|
69
|
-
},
|
|
70
|
-
body: JSON.stringify({})
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
throw new Error('Failed to delete payment method');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return await response.json();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function createSubscription(userId: string, priceId: string, paymentMethodId: string, apiUrl: string, apiKey: string) {
|
|
81
|
-
try {
|
|
82
|
-
const response = await fetch(`${apiUrl}/billing/subscriptions`, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: {
|
|
85
|
-
'x-api-key': apiKey,
|
|
86
|
-
'Content-Type': 'application/json'
|
|
87
|
-
},
|
|
88
|
-
body: JSON.stringify({
|
|
89
|
-
user_id: userId,
|
|
90
|
-
priceId: priceId,
|
|
91
|
-
paymentMethodId: paymentMethodId
|
|
92
|
-
})
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const data = await response.json();
|
|
96
|
-
|
|
97
|
-
// Check if the response indicates success
|
|
98
|
-
if (!response.ok || data.success === false) {
|
|
99
|
-
// Handle specific error types
|
|
100
|
-
let errorMessage = 'Failed to create subscription';
|
|
101
|
-
|
|
102
|
-
if (data.error === 'MISSING_PARAMETERS') {
|
|
103
|
-
errorMessage = data.message || 'Missing required parameters. Please check your information and try again.';
|
|
104
|
-
} else if (data.message) {
|
|
105
|
-
errorMessage = data.message;
|
|
106
|
-
} else if (data.error) {
|
|
107
|
-
errorMessage = data.error;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
success: false,
|
|
112
|
-
error: errorMessage,
|
|
113
|
-
errorType: data.error
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { success: true, data };
|
|
118
|
-
} catch (error) {
|
|
119
|
-
return {
|
|
120
|
-
success: false,
|
|
121
|
-
error: error instanceof Error ? error.message : 'An error occurred while creating subscription',
|
|
122
|
-
errorType: 'NETWORK_ERROR'
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function createSetupIntent(userId: string, apiUrl: string, apiKey: string) {
|
|
128
|
-
const response = await fetch(`${apiUrl}/billing/setup-intent`, {
|
|
129
|
-
method: 'POST',
|
|
130
|
-
headers: {
|
|
131
|
-
'x-api-key': apiKey,
|
|
132
|
-
'Content-Type': 'application/json'
|
|
133
|
-
},
|
|
134
|
-
body: JSON.stringify({
|
|
135
|
-
user_id: userId
|
|
136
|
-
})
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (!response.ok) {
|
|
140
|
-
throw new Error('Failed to create setup intent');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return await response.json();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export default function BillingClient({ customerData, pricingData, paymentMethods, apiUrl, apiKey }: BillingClientProps) {
|
|
147
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
148
|
-
const [addPaymentModalOpen, setAddPaymentModalOpen] = useState(false);
|
|
149
|
-
const [subscribeModalOpen, setSubscribeModalOpen] = useState(false);
|
|
150
|
-
const [deletePaymentModalOpen, setDeletePaymentModalOpen] = useState(false);
|
|
151
|
-
const [selectedPlan, setSelectedPlan] = useState<any>(null);
|
|
152
|
-
const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState<string | null>(null);
|
|
153
|
-
const [activeTab, setActiveTab] = useState('overview');
|
|
154
|
-
const router = useRouter();
|
|
155
|
-
|
|
156
|
-
const formatFeatureValue = (value: any) => {
|
|
157
|
-
// Handle string boolean values
|
|
158
|
-
if (value === 'true' || value === true) {
|
|
159
|
-
return '✓'
|
|
160
|
-
}
|
|
161
|
-
if (value === 'false' || value === false) {
|
|
162
|
-
return '✗'
|
|
163
|
-
}
|
|
164
|
-
// Handle numeric values (both string and number)
|
|
165
|
-
const numValue = typeof value === 'string' ? parseFloat(value) : value
|
|
166
|
-
if (!isNaN(numValue) && numValue >= 1000) {
|
|
167
|
-
return new Intl.NumberFormat('en-US').format(numValue)
|
|
168
|
-
}
|
|
169
|
-
return value
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const currentSubscription = customerData?.subscriptions?.[0];
|
|
173
|
-
const currentPlanItem = currentSubscription?.items?.[0];
|
|
174
|
-
const usage = customerData?.usage;
|
|
175
|
-
const invoices = customerData?.invoices || [];
|
|
176
|
-
const entitlements = customerData?.entitlements || [];
|
|
177
|
-
|
|
178
|
-
// Find the current plan details from pricing data
|
|
179
|
-
const currentPlan = pricingData?.products?.find((product: any) =>
|
|
180
|
-
product.prices?.some((price: any) => price.id === currentPlanItem?.price_id)
|
|
181
|
-
);
|
|
182
|
-
const currentPrice = currentPlan?.prices?.find((price: any) => price.id === currentPlanItem?.price_id);
|
|
183
|
-
|
|
184
|
-
const formatDate = (date: string | number) => {
|
|
185
|
-
const dateObj = typeof date === 'string' ? new Date(date) : new Date(date * 1000);
|
|
186
|
-
return dateObj.toLocaleDateString('en-US', {
|
|
187
|
-
month: 'long',
|
|
188
|
-
day: 'numeric',
|
|
189
|
-
year: 'numeric'
|
|
190
|
-
});
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const formatCurrency = (amount: number) => {
|
|
194
|
-
return new Intl.NumberFormat('en-US', {
|
|
195
|
-
style: 'currency',
|
|
196
|
-
currency: 'usd',
|
|
197
|
-
}).format(amount / 100);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const handleCancelSubscription = async () => {
|
|
201
|
-
if (!currentSubscription?.id) return;
|
|
202
|
-
|
|
203
|
-
if (!confirm('Are you sure you want to cancel your subscription? You will continue to have access until the end of your billing period.')) {
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
setIsLoading(true);
|
|
208
|
-
try {
|
|
209
|
-
await cancelSubscription(currentSubscription.id, apiUrl, apiKey);
|
|
210
|
-
router.refresh();
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error('Failed to cancel subscription:', error);
|
|
213
|
-
} finally {
|
|
214
|
-
setIsLoading(false);
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const handleSetDefaultPayment = async (paymentMethodId: string) => {
|
|
219
|
-
setIsLoading(true);
|
|
220
|
-
try {
|
|
221
|
-
await setDefaultPaymentMethod(paymentMethodId, apiUrl, apiKey);
|
|
222
|
-
router.refresh();
|
|
223
|
-
} catch (error) {
|
|
224
|
-
console.error('Failed to set default payment method:', error);
|
|
225
|
-
} finally {
|
|
226
|
-
setIsLoading(false);
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const handleDeletePayment = (paymentMethodId: string) => {
|
|
231
|
-
setSelectedPaymentMethodId(paymentMethodId);
|
|
232
|
-
setDeletePaymentModalOpen(true);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const confirmDeletePayment = async () => {
|
|
236
|
-
if (!selectedPaymentMethodId) return;
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
setIsLoading(true);
|
|
240
|
-
await deletePaymentMethod(selectedPaymentMethodId, apiUrl, apiKey);
|
|
241
|
-
router.refresh();
|
|
242
|
-
} catch (error) {
|
|
243
|
-
console.error('Error deleting payment method:', error);
|
|
244
|
-
} finally {
|
|
245
|
-
setIsLoading(false);
|
|
246
|
-
setDeletePaymentModalOpen(false);
|
|
247
|
-
setSelectedPaymentMethodId(null);
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const handlePaymentMethodAdded = () => {
|
|
252
|
-
setAddPaymentModalOpen(false);
|
|
253
|
-
router.refresh();
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const handleSubscribeClick = (plan: any) => {
|
|
257
|
-
const price = plan.prices?.[0];
|
|
258
|
-
const isFree = price && parseFloat(price.unit_amount) === 0;
|
|
259
|
-
|
|
260
|
-
if (!isFree && (!paymentMethods?.paymentMethods || paymentMethods.paymentMethods.length === 0)) {
|
|
261
|
-
setActiveTab('overview');
|
|
262
|
-
setAddPaymentModalOpen(true);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
setSelectedPlan(plan);
|
|
267
|
-
setSubscribeModalOpen(true);
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const handleSubscriptionCreated = () => {
|
|
271
|
-
setSubscribeModalOpen(false);
|
|
272
|
-
setSelectedPlan(null);
|
|
273
|
-
router.refresh();
|
|
274
|
-
setActiveTab('overview');
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const handleCreateSubscription = async (userId: string, priceId: string, paymentMethodId: string) => {
|
|
278
|
-
return await createSubscription(userId, priceId, paymentMethodId, apiUrl, apiKey);
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const handleCreateSetupIntent = async (userId: string) => {
|
|
282
|
-
return await createSetupIntent(userId, apiUrl, apiKey);
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
return (
|
|
286
|
-
<>
|
|
287
|
-
<div className="space-y-6">
|
|
288
|
-
<div>
|
|
289
|
-
<h3 className="text-lg font-medium">Billing</h3>
|
|
290
|
-
<p className="text-sm text-muted-foreground">
|
|
291
|
-
Manage your subscription and billing information.
|
|
292
|
-
</p>
|
|
293
|
-
</div>
|
|
294
|
-
|
|
295
|
-
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
|
296
|
-
<TabsList>
|
|
297
|
-
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
298
|
-
<TabsTrigger value="usage">Usage</TabsTrigger>
|
|
299
|
-
<TabsTrigger value="plans">Plans</TabsTrigger>
|
|
300
|
-
<TabsTrigger value="billing-history">Billing History</TabsTrigger>
|
|
301
|
-
</TabsList>
|
|
302
|
-
|
|
303
|
-
<TabsContent value="overview" className="space-y-4">
|
|
304
|
-
<Card>
|
|
305
|
-
<CardHeader>
|
|
306
|
-
<CardTitle>Current Plan</CardTitle>
|
|
307
|
-
<CardDescription>
|
|
308
|
-
{currentPlan ? `You are currently on the ${currentPlan.name}` : 'No active subscription'}
|
|
309
|
-
</CardDescription>
|
|
310
|
-
</CardHeader>
|
|
311
|
-
<CardContent>
|
|
312
|
-
{currentSubscription ? (
|
|
313
|
-
<div className="flex items-center justify-between">
|
|
314
|
-
<div>
|
|
315
|
-
<p className="text-2xl font-bold">
|
|
316
|
-
{currentPrice ? formatCurrency(parseFloat(currentPrice.unit_amount) * 100) : 'Active'}/month
|
|
317
|
-
</p>
|
|
318
|
-
<p className="text-sm text-muted-foreground">
|
|
319
|
-
Next billing date: {formatDate(currentSubscription.current_period_end)}
|
|
320
|
-
</p>
|
|
321
|
-
{currentSubscription.cancel_at_period_end && (
|
|
322
|
-
<p className="text-sm text-destructive mt-2">
|
|
323
|
-
<AlertCircle className="inline h-4 w-4 mr-1" />
|
|
324
|
-
Subscription will cancel at the end of the billing period
|
|
325
|
-
</p>
|
|
326
|
-
)}
|
|
327
|
-
</div>
|
|
328
|
-
<div className="space-x-2">
|
|
329
|
-
<Button variant="outline" onClick={() => setActiveTab('plans')}>
|
|
330
|
-
Change Plan
|
|
331
|
-
</Button>
|
|
332
|
-
{!currentSubscription.cancel_at_period_end && (
|
|
333
|
-
<Button
|
|
334
|
-
variant="outline"
|
|
335
|
-
onClick={handleCancelSubscription}
|
|
336
|
-
disabled={isLoading}
|
|
337
|
-
>
|
|
338
|
-
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
339
|
-
Cancel Subscription
|
|
340
|
-
</Button>
|
|
341
|
-
)}
|
|
342
|
-
</div>
|
|
343
|
-
</div>
|
|
344
|
-
) : (
|
|
345
|
-
<div className="text-center py-6">
|
|
346
|
-
<p className="text-muted-foreground mb-4">You don't have an active subscription</p>
|
|
347
|
-
<Button onClick={() => setActiveTab('plans')}>
|
|
348
|
-
Choose a Plan
|
|
349
|
-
</Button>
|
|
350
|
-
</div>
|
|
351
|
-
)}
|
|
352
|
-
</CardContent>
|
|
353
|
-
</Card>
|
|
354
|
-
|
|
355
|
-
<Card>
|
|
356
|
-
<CardHeader>
|
|
357
|
-
<CardTitle>Payment Methods</CardTitle>
|
|
358
|
-
<CardDescription>Manage your payment information</CardDescription>
|
|
359
|
-
</CardHeader>
|
|
360
|
-
<CardContent className="space-y-4">
|
|
361
|
-
{paymentMethods?.paymentMethods?.length > 0 ? (
|
|
362
|
-
<div className="space-y-3">
|
|
363
|
-
{paymentMethods.paymentMethods.map((method: any) => (
|
|
364
|
-
<div key={method.id} className="flex items-center justify-between p-4 border border-border rounded-lg">
|
|
365
|
-
<div className="flex items-center gap-4">
|
|
366
|
-
<CardBrandIcon brand={method.brand} className="w-16 h-10" />
|
|
367
|
-
<div className="flex-1">
|
|
368
|
-
<p className="font-medium">•••• •••• •••• {method.last4}</p>
|
|
369
|
-
<p className="text-sm text-muted-foreground">
|
|
370
|
-
Expires {method.exp_month}/{method.exp_year}
|
|
371
|
-
</p>
|
|
372
|
-
</div>
|
|
373
|
-
{method.is_default && (
|
|
374
|
-
<Badge variant="secondary">Default</Badge>
|
|
375
|
-
)}
|
|
376
|
-
</div>
|
|
377
|
-
<div className="flex items-center gap-2 ml-4">
|
|
378
|
-
{!method.is_default && (
|
|
379
|
-
<Button
|
|
380
|
-
variant="outline"
|
|
381
|
-
size="sm"
|
|
382
|
-
onClick={() => handleSetDefaultPayment(method.stripe_payment_method_id)}
|
|
383
|
-
disabled={isLoading}
|
|
384
|
-
>
|
|
385
|
-
Set as Default
|
|
386
|
-
</Button>
|
|
387
|
-
)}
|
|
388
|
-
<Button
|
|
389
|
-
variant="outline"
|
|
390
|
-
size="sm"
|
|
391
|
-
onClick={() => handleDeletePayment(method.stripe_payment_method_id)}
|
|
392
|
-
disabled={isLoading}
|
|
393
|
-
>
|
|
394
|
-
Remove
|
|
395
|
-
</Button>
|
|
396
|
-
</div>
|
|
397
|
-
</div>
|
|
398
|
-
))}
|
|
399
|
-
</div>
|
|
400
|
-
) : (
|
|
401
|
-
<div className="text-center py-6">
|
|
402
|
-
<p className="text-muted-foreground mb-4">No payment methods on file</p>
|
|
403
|
-
</div>
|
|
404
|
-
)}
|
|
405
|
-
</CardContent>
|
|
406
|
-
<CardFooter>
|
|
407
|
-
<Button className="w-full" onClick={() => setAddPaymentModalOpen(true)}>
|
|
408
|
-
<CreditCard className="mr-2 h-4 w-4" />
|
|
409
|
-
Add Payment Method
|
|
410
|
-
</Button>
|
|
411
|
-
</CardFooter>
|
|
412
|
-
</Card>
|
|
413
|
-
</TabsContent>
|
|
414
|
-
|
|
415
|
-
<TabsContent value="usage" className="space-y-4">
|
|
416
|
-
<Card>
|
|
417
|
-
<CardHeader>
|
|
418
|
-
<CardTitle>Current Usage</CardTitle>
|
|
419
|
-
<CardDescription>Your resource usage for the current billing period</CardDescription>
|
|
420
|
-
</CardHeader>
|
|
421
|
-
<CardContent className="space-y-6">
|
|
422
|
-
{usage?.data?.filter((item: any) => typeof item.limit === 'number' && item.limit > 0).map((item: any) => {
|
|
423
|
-
const percentage = typeof item.limit === 'number'
|
|
424
|
-
? Math.min((item.current_usage / item.limit) * 100, 100)
|
|
425
|
-
: 0;
|
|
426
|
-
|
|
427
|
-
const getUsageColor = (percent: number) => {
|
|
428
|
-
if (percent >= 90) return 'text-red-600 dark:text-red-400';
|
|
429
|
-
if (percent >= 75) return 'text-orange-600 dark:text-orange-400';
|
|
430
|
-
if (percent >= 50) return 'text-yellow-600 dark:text-yellow-400';
|
|
431
|
-
return 'text-green-600 dark:text-green-400';
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const getProgressColor = (percent: number) => {
|
|
435
|
-
if (percent >= 90) return 'bg-red-100 dark:bg-red-950';
|
|
436
|
-
if (percent >= 75) return 'bg-orange-100 dark:bg-orange-950';
|
|
437
|
-
if (percent >= 50) return 'bg-yellow-100 dark:bg-yellow-950';
|
|
438
|
-
return 'bg-green-100 dark:bg-green-950';
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const getStatusIcon = (percent: number, withinLimit: boolean) => {
|
|
442
|
-
if (!withinLimit || percent >= 90) {
|
|
443
|
-
return <AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />;
|
|
444
|
-
}
|
|
445
|
-
if (percent >= 75) {
|
|
446
|
-
return <AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />;
|
|
447
|
-
}
|
|
448
|
-
if (percent >= 50) {
|
|
449
|
-
return <Info className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />;
|
|
450
|
-
}
|
|
451
|
-
return <CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />;
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
return (
|
|
455
|
-
<div key={item.feature_key} className="space-y-3">
|
|
456
|
-
<div className="flex items-center justify-between text-sm">
|
|
457
|
-
<div className="flex items-center gap-2">
|
|
458
|
-
{typeof item.limit === 'number' && getStatusIcon(percentage, item.within_limit)}
|
|
459
|
-
<span>{item.feature_details.description}</span>
|
|
460
|
-
</div>
|
|
461
|
-
<span className={`font-medium ${typeof item.limit === 'number' ? getUsageColor(percentage) : ''}`}>
|
|
462
|
-
{typeof item.limit === 'number'
|
|
463
|
-
? `${item.current_usage.toLocaleString()} / ${item.limit.toLocaleString()}`
|
|
464
|
-
: item.current_usage
|
|
465
|
-
}
|
|
466
|
-
{item.feature_details.units && ` ${item.feature_details.units}`}
|
|
467
|
-
</span>
|
|
468
|
-
</div>
|
|
469
|
-
{typeof item.limit === 'number' && (
|
|
470
|
-
<>
|
|
471
|
-
<Progress value={percentage} className={`h-2 ${getProgressColor(percentage)} ${!item.within_limit ? '[&>div]:bg-red-500' : percentage >= 90 ? '[&>div]:bg-red-500' : percentage >= 75 ? '[&>div]:bg-orange-500' : percentage >= 50 ? '[&>div]:bg-yellow-500' : '[&>div]:bg-green-500'}`} />
|
|
472
|
-
<p className={`text-xs ${item.within_limit ? percentage >= 90 ? 'text-red-600 dark:text-red-400' : percentage >= 75 ? 'text-orange-600 dark:text-orange-400' : 'text-muted-foreground' : 'text-red-600 dark:text-red-400 font-medium'}`}>
|
|
473
|
-
{item.within_limit
|
|
474
|
-
? `${Math.max(0, item.limit - item.current_usage).toLocaleString()} ${item.feature_details.units || ''} remaining`
|
|
475
|
-
: `${(item.current_usage - item.limit).toLocaleString()} ${item.feature_details.units || ''} over limit`
|
|
476
|
-
}
|
|
477
|
-
</p>
|
|
478
|
-
</>
|
|
479
|
-
)}
|
|
480
|
-
</div>
|
|
481
|
-
);
|
|
482
|
-
})}
|
|
483
|
-
</CardContent>
|
|
484
|
-
<CardFooter>
|
|
485
|
-
<p className="text-sm text-muted-foreground">
|
|
486
|
-
Usage resets on {currentSubscription ? formatDate(currentSubscription.current_period_end) : 'N/A'}.
|
|
487
|
-
Need more resources? Consider upgrading your plan.
|
|
488
|
-
</p>
|
|
489
|
-
</CardFooter>
|
|
490
|
-
</Card>
|
|
491
|
-
</TabsContent>
|
|
492
|
-
|
|
493
|
-
<TabsContent value="plans" className="space-y-4">
|
|
494
|
-
<div className={`grid gap-4 ${pricingData?.products?.length === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'}`}>
|
|
495
|
-
{pricingData?.products?.map((product: any) => {
|
|
496
|
-
const price = product.prices?.[0];
|
|
497
|
-
const features = product.features || [];
|
|
498
|
-
const isCurrentPlan = currentPlanItem?.price_id === price?.id;
|
|
499
|
-
|
|
500
|
-
// Determine if this is an upgrade, downgrade, or switch to free
|
|
501
|
-
const getButtonText = () => {
|
|
502
|
-
if (isCurrentPlan) return "Current Plan";
|
|
503
|
-
|
|
504
|
-
const targetPrice = parseFloat(price?.unit_amount || 0);
|
|
505
|
-
const currentPlanPrice = parseFloat(currentPrice?.unit_amount || 0);
|
|
506
|
-
|
|
507
|
-
// If target is free plan
|
|
508
|
-
if (targetPrice === 0) {
|
|
509
|
-
return currentPlanPrice > 0 ? "Switch to Free" : "Get Started";
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// If current plan is free
|
|
513
|
-
if (currentPlanPrice === 0) {
|
|
514
|
-
return "Upgrade";
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Compare prices for paid plans
|
|
518
|
-
return targetPrice > currentPlanPrice ? "Upgrade" : "Downgrade";
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
return (
|
|
522
|
-
<Card key={product.id} className={`pricing-card relative overflow-hidden h-full flex flex-col ${isCurrentPlan ? "current border-primary shadow-md" : "border-muted"} ${product.metadata?.popular === 'true' && !isCurrentPlan ? "border-primary/50" : ""}`}>
|
|
523
|
-
{product.metadata?.popular === 'true' && !isCurrentPlan && (
|
|
524
|
-
<div className="absolute top-0 right-0 bg-primary text-primary-foreground text-xs font-medium px-3 py-1 rounded-bl-lg">
|
|
525
|
-
POPULAR
|
|
526
|
-
</div>
|
|
527
|
-
)}
|
|
528
|
-
<CardHeader className="text-center pb-4 pt-4">
|
|
529
|
-
<CardTitle className="text-lg font-semibold">{product.name}</CardTitle>
|
|
530
|
-
<CardDescription className="mt-1 text-sm">{product.description}</CardDescription>
|
|
531
|
-
</CardHeader>
|
|
532
|
-
<CardContent className="text-center pb-4 flex-1">
|
|
533
|
-
<div className="mb-4">
|
|
534
|
-
<div className="flex items-baseline justify-center">
|
|
535
|
-
<span className="text-3xl font-bold tracking-tight">
|
|
536
|
-
{parseFloat(price?.unit_amount) === 0 ? 'Free' : `$${parseFloat(price?.unit_amount).toFixed(2).replace(/\.00$/, '')}`}
|
|
537
|
-
</span>
|
|
538
|
-
{parseFloat(price?.unit_amount) > 0 && (
|
|
539
|
-
<span className="ml-1 text-sm text-muted-foreground">/month</span>
|
|
540
|
-
)}
|
|
541
|
-
</div>
|
|
542
|
-
{isCurrentPlan ? (
|
|
543
|
-
<Badge variant="secondary" className="mt-2">Current Plan</Badge>
|
|
544
|
-
) : (
|
|
545
|
-
<div className="mt-2 h-6"></div>
|
|
546
|
-
)}
|
|
547
|
-
</div>
|
|
548
|
-
<div className="space-y-3 flex-1 flex flex-col justify-between min-h-[300px]">
|
|
549
|
-
{features.map((feature: any) => (
|
|
550
|
-
<div key={feature.id} className="border-t pt-3 first:border-t-0 first:pt-0 flex-1 flex flex-col justify-center">
|
|
551
|
-
<div className="text-lg font-semibold text-foreground">
|
|
552
|
-
{formatFeatureValue(feature.value)} <span className="text-sm text-gray-500 font-normal">{feature.units}</span>
|
|
553
|
-
</div>
|
|
554
|
-
<TooltipProvider>
|
|
555
|
-
<Tooltip>
|
|
556
|
-
<TooltipTrigger asChild>
|
|
557
|
-
<div className="text-xs text-gray-500 mt-1 cursor-help">
|
|
558
|
-
{feature.name || feature.description}
|
|
559
|
-
</div>
|
|
560
|
-
</TooltipTrigger>
|
|
561
|
-
<TooltipContent>
|
|
562
|
-
<p>{feature.description}</p>
|
|
563
|
-
</TooltipContent>
|
|
564
|
-
</Tooltip>
|
|
565
|
-
</TooltipProvider>
|
|
566
|
-
</div>
|
|
567
|
-
))}
|
|
568
|
-
</div>
|
|
569
|
-
</CardContent>
|
|
570
|
-
<CardFooter className="pt-3">
|
|
571
|
-
<Button
|
|
572
|
-
className="w-full"
|
|
573
|
-
variant={isCurrentPlan ? "outline" : product.metadata?.popular === 'true' ? "default" : "secondary"}
|
|
574
|
-
size="lg"
|
|
575
|
-
disabled={isCurrentPlan}
|
|
576
|
-
onClick={() => !isCurrentPlan && handleSubscribeClick(product)}
|
|
577
|
-
>
|
|
578
|
-
{getButtonText()}
|
|
579
|
-
</Button>
|
|
580
|
-
</CardFooter>
|
|
581
|
-
</Card>
|
|
582
|
-
);
|
|
583
|
-
})}
|
|
584
|
-
</div>
|
|
585
|
-
</TabsContent>
|
|
586
|
-
|
|
587
|
-
<TabsContent value="billing-history" className="space-y-4">
|
|
588
|
-
<Card>
|
|
589
|
-
<CardHeader>
|
|
590
|
-
<CardTitle>Billing History</CardTitle>
|
|
591
|
-
<CardDescription>Download your past invoices</CardDescription>
|
|
592
|
-
</CardHeader>
|
|
593
|
-
<CardContent>
|
|
594
|
-
<div className="space-y-4">
|
|
595
|
-
{invoices.length > 0 ? (
|
|
596
|
-
invoices.map((invoice: any) => (
|
|
597
|
-
<div key={invoice.id} className="flex items-center justify-between p-4 border border-border rounded-lg">
|
|
598
|
-
<div className="flex items-center space-x-4">
|
|
599
|
-
<FileText className="h-8 w-8 text-muted-foreground" />
|
|
600
|
-
<div>
|
|
601
|
-
<p className="font-medium">Invoice #{invoice.id.slice(-8).toUpperCase()}</p>
|
|
602
|
-
<p className="text-sm text-muted-foreground">{formatDate(invoice.created)}</p>
|
|
603
|
-
</div>
|
|
604
|
-
</div>
|
|
605
|
-
<div className="flex items-center space-x-4">
|
|
606
|
-
<div className="text-right">
|
|
607
|
-
<p className="font-medium">{formatCurrency(invoice.amount_paid)}</p>
|
|
608
|
-
<p className="text-sm text-muted-foreground capitalize">{invoice.status}</p>
|
|
609
|
-
</div>
|
|
610
|
-
<Button
|
|
611
|
-
variant="outline"
|
|
612
|
-
size="sm"
|
|
613
|
-
onClick={() => window.open(invoice.invoice_pdf || invoice.hosted_invoice_url, '_blank')}
|
|
614
|
-
>
|
|
615
|
-
<FileText className="h-4 w-4 mr-2" />
|
|
616
|
-
Download
|
|
617
|
-
</Button>
|
|
618
|
-
</div>
|
|
619
|
-
</div>
|
|
620
|
-
))
|
|
621
|
-
) : (
|
|
622
|
-
<div className="text-center py-12">
|
|
623
|
-
<FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
|
624
|
-
<p className="text-muted-foreground">No invoices yet</p>
|
|
625
|
-
</div>
|
|
626
|
-
)}
|
|
627
|
-
</div>
|
|
628
|
-
</CardContent>
|
|
629
|
-
</Card>
|
|
630
|
-
</TabsContent>
|
|
631
|
-
</Tabs>
|
|
632
|
-
</div>
|
|
633
|
-
|
|
634
|
-
<Dialog open={addPaymentModalOpen} onOpenChange={setAddPaymentModalOpen}>
|
|
635
|
-
<DialogContent>
|
|
636
|
-
<DialogHeader>
|
|
637
|
-
<DialogTitle>Add Payment Method</DialogTitle>
|
|
638
|
-
<DialogDescription>
|
|
639
|
-
Add a new payment method to your account. Your card information is securely processed by Stripe.
|
|
640
|
-
</DialogDescription>
|
|
641
|
-
</DialogHeader>
|
|
642
|
-
|
|
643
|
-
<Elements stripe={stripePromise}>
|
|
644
|
-
<PaymentMethodForm
|
|
645
|
-
userId={String(customerData?.id || '')}
|
|
646
|
-
onSuccess={handlePaymentMethodAdded}
|
|
647
|
-
onCancel={() => setAddPaymentModalOpen(false)}
|
|
648
|
-
createSetupIntent={handleCreateSetupIntent}
|
|
649
|
-
/>
|
|
650
|
-
</Elements>
|
|
651
|
-
</DialogContent>
|
|
652
|
-
</Dialog>
|
|
653
|
-
|
|
654
|
-
<SubscriptionConfirmModal
|
|
655
|
-
isOpen={subscribeModalOpen}
|
|
656
|
-
onClose={() => setSubscribeModalOpen(false)}
|
|
657
|
-
plan={selectedPlan}
|
|
658
|
-
currentPlan={currentPlan}
|
|
659
|
-
paymentMethods={paymentMethods?.paymentMethods || []}
|
|
660
|
-
userId={String(customerData?.id || '')}
|
|
661
|
-
onSuccess={handleSubscriptionCreated}
|
|
662
|
-
createSubscription={handleCreateSubscription}
|
|
663
|
-
/>
|
|
664
|
-
|
|
665
|
-
<Dialog open={deletePaymentModalOpen} onOpenChange={setDeletePaymentModalOpen}>
|
|
666
|
-
<DialogContent>
|
|
667
|
-
<DialogHeader>
|
|
668
|
-
<DialogTitle>Delete Payment Method</DialogTitle>
|
|
669
|
-
<DialogDescription>
|
|
670
|
-
Are you sure you want to delete this payment method? This action cannot be undone.
|
|
671
|
-
</DialogDescription>
|
|
672
|
-
</DialogHeader>
|
|
673
|
-
<div className="flex justify-end space-x-2 pt-4">
|
|
674
|
-
<Button variant="outline" onClick={() => setDeletePaymentModalOpen(false)}>
|
|
675
|
-
Cancel
|
|
676
|
-
</Button>
|
|
677
|
-
<Button variant="destructive" onClick={confirmDeletePayment} disabled={isLoading}>
|
|
678
|
-
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
679
|
-
Delete
|
|
680
|
-
</Button>
|
|
681
|
-
</div>
|
|
682
|
-
</DialogContent>
|
|
683
|
-
</Dialog>
|
|
684
|
-
</>
|
|
685
|
-
);
|
|
686
|
-
}
|