vibefast-cli 1.3.3 → 1.3.4
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 +4 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +317 -6
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +38 -10
- package/dist/commands/init.js.map +1 -1
- package/docs/commands.md +2 -0
- package/docs/quickstart.md +1 -1
- package/docs/recipes.md +2 -2
- package/package.json +1 -1
- package/recipes/audio-recorder-native@latest.zip +0 -0
- package/recipes/audio-recorder-supabase-native@latest.zip +0 -0
- package/recipes/chatbot-native@latest.zip +0 -0
- package/recipes/chatbot-supabase-native@latest.zip +0 -0
- package/recipes/glowing-button-native@latest.zip +0 -0
- package/recipes/image-analysis-native@latest.zip +0 -0
- package/recipes/image-analysis-supabase-native@latest.zip +0 -0
- package/recipes/image-generator-native@latest.zip +0 -0
- package/recipes/ios-widget-native@latest.zip +0 -0
- package/recipes/lemonsqueezy/apps/web/app/billing/page.tsx +48 -0
- package/recipes/lemonsqueezy/apps/web/app/pricing/page.tsx +22 -0
- package/recipes/lemonsqueezy/apps/web/components/payments/BillingSection.tsx +161 -0
- package/recipes/lemonsqueezy/apps/web/components/payments/PricingCard.tsx +87 -0
- package/recipes/lemonsqueezy/apps/web/components/payments/PricingSection.tsx +85 -0
- package/recipes/lemonsqueezy/apps/web/components/payments/index.ts +10 -0
- package/recipes/lemonsqueezy/apps/web/lib/plans.ts +84 -0
- package/recipes/lemonsqueezy/packages/backend/convex/payments/config.ts +127 -0
- package/recipes/lemonsqueezy/packages/backend/convex/payments/index.ts +52 -0
- package/recipes/lemonsqueezy/packages/backend/convex/payments/providers/lemonsqueezy.ts +317 -0
- package/recipes/lemonsqueezy/packages/backend/convex/payments/types.ts +89 -0
- package/recipes/lemonsqueezy/packages/backend/convex/payments/webhooks.ts +187 -0
- package/recipes/lemonsqueezy/packages/backend/convex/payments.ts +137 -0
- package/recipes/lemonsqueezy/recipe.json +120 -0
- package/recipes/lemonsqueezy-web@latest.zip +0 -0
- package/recipes/payments-native@latest.zip +0 -0
- package/recipes/payments-supabase-native@latest.zip +0 -0
- package/recipes/stripe/apps/web/app/billing/page.tsx +48 -0
- package/recipes/stripe/apps/web/app/pricing/page.tsx +22 -0
- package/recipes/stripe/apps/web/components/payments/BillingSection.tsx +161 -0
- package/recipes/stripe/apps/web/components/payments/PricingCard.tsx +87 -0
- package/recipes/stripe/apps/web/components/payments/PricingSection.tsx +85 -0
- package/recipes/stripe/apps/web/components/payments/index.ts +10 -0
- package/recipes/stripe/apps/web/lib/plans.ts +84 -0
- package/recipes/stripe/packages/backend/convex/payments/config.ts +127 -0
- package/recipes/stripe/packages/backend/convex/payments/index.ts +52 -0
- package/recipes/stripe/packages/backend/convex/payments/providers/stripe.ts +186 -0
- package/recipes/stripe/packages/backend/convex/payments/types.ts +89 -0
- package/recipes/stripe/packages/backend/convex/payments.ts +137 -0
- package/recipes/stripe/recipe.json +114 -0
- package/recipes/stripe-web@latest.zip +0 -0
- package/recipes/voice-bot-native@latest.zip +0 -0
- package/recipes/wake-word-native@latest.zip +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAction, useQuery } from 'convex/react';
|
|
4
|
+
import { api } from '@vibefast/backend/_generated/api';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { CreditCard, ExternalLink, Settings, AlertCircle } from 'lucide-react';
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Billing Section Component
|
|
11
|
+
// Displays user's subscription status and billing management options
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export function BillingSection() {
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
const subscription = useQuery(api.payments.getSubscription);
|
|
19
|
+
const createPortalSession = useAction(api.payments.createPortalSession);
|
|
20
|
+
|
|
21
|
+
const cancelAt =
|
|
22
|
+
typeof subscription?.cancelAt === 'number' ? subscription.cancelAt : null;
|
|
23
|
+
const hasActiveSubscription =
|
|
24
|
+
subscription?.status === 'active' || subscription?.status === 'trialing';
|
|
25
|
+
const isCanceling = Boolean(subscription?.cancelAtPeriodEnd || cancelAt);
|
|
26
|
+
|
|
27
|
+
const handleManageBilling = async () => {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await createPortalSession({
|
|
33
|
+
returnUrl: window.location.href,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (result.url) {
|
|
37
|
+
window.location.href = result.url;
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('Portal error:', err);
|
|
41
|
+
setError(
|
|
42
|
+
err instanceof Error ? err.message : 'Failed to open billing portal',
|
|
43
|
+
);
|
|
44
|
+
} finally {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const formatDate = (timestamp?: number) => {
|
|
50
|
+
if (!timestamp) return 'N/A';
|
|
51
|
+
return new Date(timestamp * 1000).toLocaleDateString(undefined, {
|
|
52
|
+
year: 'numeric',
|
|
53
|
+
month: 'long',
|
|
54
|
+
day: 'numeric',
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-6">
|
|
60
|
+
{/* Current Subscription */}
|
|
61
|
+
<div className="rounded-lg border bg-card p-6">
|
|
62
|
+
<div className="flex items-center justify-between">
|
|
63
|
+
<div className="flex items-center gap-3">
|
|
64
|
+
<CreditCard className="h-5 w-5 text-muted-foreground" />
|
|
65
|
+
<h3 className="font-semibold">Subscription</h3>
|
|
66
|
+
</div>
|
|
67
|
+
<span
|
|
68
|
+
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${
|
|
69
|
+
hasActiveSubscription
|
|
70
|
+
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
|
|
71
|
+
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'
|
|
72
|
+
}`}
|
|
73
|
+
>
|
|
74
|
+
{hasActiveSubscription
|
|
75
|
+
? subscription?.status === 'trialing'
|
|
76
|
+
? 'Trial'
|
|
77
|
+
: isCanceling
|
|
78
|
+
? 'Canceling'
|
|
79
|
+
: 'Active'
|
|
80
|
+
: 'No Plan'}
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{subscription && hasActiveSubscription && (
|
|
85
|
+
<div className="mt-4 rounded-md bg-muted p-4">
|
|
86
|
+
<div className="grid gap-2 text-sm">
|
|
87
|
+
<div className="flex justify-between">
|
|
88
|
+
<span className="text-muted-foreground">Plan</span>
|
|
89
|
+
<span className="font-medium">
|
|
90
|
+
{subscription.planId || 'Custom Plan'}
|
|
91
|
+
</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="flex justify-between">
|
|
94
|
+
<span className="text-muted-foreground">
|
|
95
|
+
{isCanceling ? 'Ends' : 'Renews'}
|
|
96
|
+
</span>
|
|
97
|
+
<span className="font-medium">
|
|
98
|
+
{formatDate(
|
|
99
|
+
isCanceling && cancelAt
|
|
100
|
+
? cancelAt
|
|
101
|
+
: subscription.currentPeriodEnd,
|
|
102
|
+
)}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
{isCanceling && (
|
|
106
|
+
<div className="mt-2 flex items-center gap-2 text-amber-600 dark:text-amber-400">
|
|
107
|
+
<AlertCircle className="h-4 w-4" />
|
|
108
|
+
<span className="text-xs">Scheduled to cancel</span>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{!hasActiveSubscription && (
|
|
116
|
+
<p className="mt-4 text-sm text-muted-foreground">
|
|
117
|
+
You don't have an active subscription. Choose a plan to get
|
|
118
|
+
started.
|
|
119
|
+
</p>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Actions */}
|
|
124
|
+
{error && (
|
|
125
|
+
<div className="rounded-lg border border-destructive bg-destructive/10 p-4 text-sm text-destructive">
|
|
126
|
+
{error}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
<div className="flex flex-col gap-3 sm:flex-row">
|
|
131
|
+
{hasActiveSubscription && (
|
|
132
|
+
<Button
|
|
133
|
+
onClick={handleManageBilling}
|
|
134
|
+
disabled={loading}
|
|
135
|
+
variant="outline"
|
|
136
|
+
className="flex-1"
|
|
137
|
+
>
|
|
138
|
+
{loading ? (
|
|
139
|
+
<span className="flex items-center gap-2">
|
|
140
|
+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
141
|
+
Opening...
|
|
142
|
+
</span>
|
|
143
|
+
) : (
|
|
144
|
+
<>
|
|
145
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
146
|
+
Manage Billing
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</Button>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<Button variant={hasActiveSubscription ? 'outline' : 'default'} asChild>
|
|
153
|
+
<a href="/pricing">
|
|
154
|
+
<ExternalLink className="mr-2 h-4 w-4" />
|
|
155
|
+
{hasActiveSubscription ? 'Change Plan' : 'View Plans'}
|
|
156
|
+
</a>
|
|
157
|
+
</Button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Check } from 'lucide-react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Pricing Card Component
|
|
9
|
+
// Reusable card for displaying a single pricing plan
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
interface PricingCardProps {
|
|
13
|
+
name: string;
|
|
14
|
+
price: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
features: string[];
|
|
17
|
+
popular?: boolean;
|
|
18
|
+
onSelect: () => void;
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
buttonText?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function PricingCard({
|
|
25
|
+
name,
|
|
26
|
+
price,
|
|
27
|
+
description,
|
|
28
|
+
features,
|
|
29
|
+
popular,
|
|
30
|
+
onSelect,
|
|
31
|
+
loading,
|
|
32
|
+
buttonText = 'Get Started',
|
|
33
|
+
disabled,
|
|
34
|
+
}: PricingCardProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={cn(
|
|
38
|
+
'relative flex flex-col rounded-2xl border bg-card p-6 shadow-sm transition-all',
|
|
39
|
+
popular && 'border-primary shadow-lg ring-1 ring-primary',
|
|
40
|
+
'hover:shadow-md',
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{popular && (
|
|
44
|
+
<div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground">
|
|
45
|
+
Most Popular
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div className="mb-4">
|
|
50
|
+
<h3 className="text-lg font-semibold">{name}</h3>
|
|
51
|
+
{description && (
|
|
52
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="mb-6">
|
|
57
|
+
<span className="text-3xl font-bold">{price}</span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<ul className="mb-6 flex-1 space-y-3">
|
|
61
|
+
{features.map((feature, index) => (
|
|
62
|
+
<li key={index} className="flex items-start gap-2 text-sm">
|
|
63
|
+
<Check className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
|
64
|
+
<span>{feature}</span>
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
|
|
69
|
+
<Button
|
|
70
|
+
onClick={onSelect}
|
|
71
|
+
disabled={loading || disabled}
|
|
72
|
+
className="w-full"
|
|
73
|
+
variant={popular ? 'default' : 'outline'}
|
|
74
|
+
size="lg"
|
|
75
|
+
>
|
|
76
|
+
{loading ? (
|
|
77
|
+
<span className="flex items-center gap-2">
|
|
78
|
+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
79
|
+
Processing...
|
|
80
|
+
</span>
|
|
81
|
+
) : (
|
|
82
|
+
buttonText
|
|
83
|
+
)}
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useAction } from 'convex/react';
|
|
5
|
+
import { api } from '@vibefast/backend/_generated/api';
|
|
6
|
+
import { PricingCard } from './PricingCard';
|
|
7
|
+
import { PLANS, type Plan } from '@/lib/plans';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Pricing Section Component
|
|
11
|
+
// Displays all available pricing plans with checkout functionality
|
|
12
|
+
// Plans are imported from @/lib/plans.ts for single source of truth
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
interface PricingSectionProps {
|
|
16
|
+
plans?: Plan[];
|
|
17
|
+
title?: string;
|
|
18
|
+
subtitle?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function PricingSection({
|
|
22
|
+
plans = PLANS,
|
|
23
|
+
title = 'Simple, Transparent Pricing',
|
|
24
|
+
subtitle = 'Choose the plan that fits your needs. Cancel anytime.',
|
|
25
|
+
}: PricingSectionProps) {
|
|
26
|
+
const [loadingPlan, setLoadingPlan] = useState<string | null>(null);
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
|
|
29
|
+
const createCheckout = useAction(api.payments.createCheckout);
|
|
30
|
+
|
|
31
|
+
const handleSelectPlan = async (plan: (typeof PLANS)[0]) => {
|
|
32
|
+
setLoadingPlan(plan.id);
|
|
33
|
+
setError(null);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await createCheckout({
|
|
37
|
+
priceId: plan.priceId,
|
|
38
|
+
successUrl: `${window.location.origin}/billing?success=true`,
|
|
39
|
+
cancelUrl: `${window.location.origin}/pricing?canceled=true`,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (result.url) {
|
|
43
|
+
window.location.href = result.url;
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error('No checkout URL returned');
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('Checkout error:', err);
|
|
49
|
+
setError(err instanceof Error ? err.message : 'Failed to start checkout');
|
|
50
|
+
} finally {
|
|
51
|
+
setLoadingPlan(null);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<section className="py-16">
|
|
57
|
+
<div className="text-center">
|
|
58
|
+
<h2 className="text-3xl font-bold tracking-tight">{title}</h2>
|
|
59
|
+
<p className="mt-2 text-muted-foreground">{subtitle}</p>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{error && (
|
|
63
|
+
<div className="mx-auto mt-6 max-w-md rounded-lg border border-destructive bg-destructive/10 p-4 text-center text-sm text-destructive">
|
|
64
|
+
{error}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
<div className="mt-10 grid gap-6 md:grid-cols-3">
|
|
69
|
+
{plans.map((plan) => (
|
|
70
|
+
<PricingCard
|
|
71
|
+
key={plan.id}
|
|
72
|
+
name={plan.name}
|
|
73
|
+
description={plan.description}
|
|
74
|
+
price={plan.price}
|
|
75
|
+
features={plan.features}
|
|
76
|
+
popular={plan.popular}
|
|
77
|
+
loading={loadingPlan === plan.id}
|
|
78
|
+
disabled={loadingPlan !== null && loadingPlan !== plan.id}
|
|
79
|
+
onSelect={() => handleSelectPlan(plan)}
|
|
80
|
+
/>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Payment Components Barrel Export
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export { PricingCard } from './PricingCard';
|
|
6
|
+
export { PricingSection } from './PricingSection';
|
|
7
|
+
export { BillingSection } from './BillingSection';
|
|
8
|
+
|
|
9
|
+
// Re-export plan config for convenience
|
|
10
|
+
export { PLANS, type Plan, getPlanById, getPlanByPriceId } from '@/lib/plans';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Shared Plans Configuration (Frontend)
|
|
3
|
+
// Single source of truth for pricing plans in the web app
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Plan definition for pricing display
|
|
8
|
+
*/
|
|
9
|
+
export interface Plan {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
price: string;
|
|
14
|
+
priceId: string;
|
|
15
|
+
features: string[];
|
|
16
|
+
popular?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pricing plans configuration
|
|
21
|
+
* Update these with your actual pricing plans from Stripe/LemonSqueezy
|
|
22
|
+
*
|
|
23
|
+
* Note: Uses NEXT_PUBLIC_ prefixed env vars for client-side access
|
|
24
|
+
* The backend config.ts uses non-prefixed versions for server-side
|
|
25
|
+
*/
|
|
26
|
+
export const PLANS: Plan[] = [
|
|
27
|
+
{
|
|
28
|
+
id: 'starter',
|
|
29
|
+
name: 'Starter',
|
|
30
|
+
description: 'Perfect for individuals getting started',
|
|
31
|
+
price: '$9/mo',
|
|
32
|
+
priceId: process.env.NEXT_PUBLIC_PLAN_STARTER_PRICE_ID ?? 'price_starter',
|
|
33
|
+
features: [
|
|
34
|
+
'100 AI credits per month',
|
|
35
|
+
'Basic support',
|
|
36
|
+
'Single project',
|
|
37
|
+
'Community access',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'pro',
|
|
42
|
+
name: 'Pro',
|
|
43
|
+
description: 'Best for professionals and growing teams',
|
|
44
|
+
price: '$29/mo',
|
|
45
|
+
priceId: process.env.NEXT_PUBLIC_PLAN_PRO_PRICE_ID ?? 'price_pro',
|
|
46
|
+
features: [
|
|
47
|
+
'Unlimited AI credits',
|
|
48
|
+
'Priority support',
|
|
49
|
+
'Unlimited projects',
|
|
50
|
+
'API access',
|
|
51
|
+
'Advanced analytics',
|
|
52
|
+
],
|
|
53
|
+
popular: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'enterprise',
|
|
57
|
+
name: 'Enterprise',
|
|
58
|
+
description: 'For large teams with custom needs',
|
|
59
|
+
price: '$99/mo',
|
|
60
|
+
priceId:
|
|
61
|
+
process.env.NEXT_PUBLIC_PLAN_ENTERPRISE_PRICE_ID ?? 'price_enterprise',
|
|
62
|
+
features: [
|
|
63
|
+
'Everything in Pro',
|
|
64
|
+
'Dedicated support',
|
|
65
|
+
'Custom integrations',
|
|
66
|
+
'SLA guarantee',
|
|
67
|
+
'SSO & advanced security',
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get a plan by its ID
|
|
74
|
+
*/
|
|
75
|
+
export const getPlanById = (planId: string): Plan | undefined => {
|
|
76
|
+
return PLANS.find((plan) => plan.id === planId);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get a plan by its provider-specific price ID
|
|
81
|
+
*/
|
|
82
|
+
export const getPlanByPriceId = (priceId: string): Plan | undefined => {
|
|
83
|
+
return PLANS.find((plan) => plan.priceId === priceId);
|
|
84
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Plan } from './types';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Payment Configuration
|
|
5
|
+
// Define your pricing plans and provider settings here
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Supported payment providers
|
|
10
|
+
*/
|
|
11
|
+
export type PaymentProvider = 'stripe' | 'lemonsqueezy';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the active payment provider from environment
|
|
15
|
+
*/
|
|
16
|
+
export const getPaymentProvider = (): PaymentProvider => {
|
|
17
|
+
const provider = process.env.PAYMENT_PROVIDER as PaymentProvider;
|
|
18
|
+
if (!provider) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'PAYMENT_PROVIDER environment variable must be set to "stripe" or "lemonsqueezy"',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (!['stripe', 'lemonsqueezy'].includes(provider)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid PAYMENT_PROVIDER: "${provider}". Must be "stripe" or "lemonsqueezy"`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return provider;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the site URL for redirects
|
|
33
|
+
*/
|
|
34
|
+
export const getSiteUrl = (): string => {
|
|
35
|
+
const url = process.env.SITE_URL;
|
|
36
|
+
if (!url) {
|
|
37
|
+
throw new Error('SITE_URL environment variable must be set');
|
|
38
|
+
}
|
|
39
|
+
return url.replace(/\/$/, ''); // Remove trailing slash
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default success/cancel URLs for checkout
|
|
44
|
+
*/
|
|
45
|
+
export const getCheckoutUrls = (
|
|
46
|
+
customSuccess?: string,
|
|
47
|
+
customCancel?: string,
|
|
48
|
+
) => {
|
|
49
|
+
const siteUrl = getSiteUrl();
|
|
50
|
+
return {
|
|
51
|
+
successUrl: customSuccess ?? `${siteUrl}/billing?success=true`,
|
|
52
|
+
cancelUrl: customCancel ?? `${siteUrl}/pricing?canceled=true`,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Pricing Plans Configuration
|
|
58
|
+
// Update these with your actual plan details and provider-specific price IDs
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Define your pricing plans here.
|
|
63
|
+
* The `priceId` should be:
|
|
64
|
+
* - For Stripe: The Stripe Price ID (e.g., "price_1234...")
|
|
65
|
+
* - For LemonSqueezy: The Variant ID (e.g., "123456")
|
|
66
|
+
*/
|
|
67
|
+
export const PLANS: Plan[] = [
|
|
68
|
+
{
|
|
69
|
+
id: 'starter',
|
|
70
|
+
name: 'Starter',
|
|
71
|
+
description: 'Perfect for individuals getting started',
|
|
72
|
+
price: '$9/mo',
|
|
73
|
+
priceId: process.env.PLAN_STARTER_PRICE_ID ?? 'price_starter',
|
|
74
|
+
interval: 'month',
|
|
75
|
+
features: [
|
|
76
|
+
'100 AI credits per month',
|
|
77
|
+
'Basic support',
|
|
78
|
+
'Single project',
|
|
79
|
+
'Community access',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'pro',
|
|
84
|
+
name: 'Pro',
|
|
85
|
+
description: 'Best for professionals and growing teams',
|
|
86
|
+
price: '$29/mo',
|
|
87
|
+
priceId: process.env.PLAN_PRO_PRICE_ID ?? 'price_pro',
|
|
88
|
+
interval: 'month',
|
|
89
|
+
features: [
|
|
90
|
+
'Unlimited AI credits',
|
|
91
|
+
'Priority support',
|
|
92
|
+
'Unlimited projects',
|
|
93
|
+
'API access',
|
|
94
|
+
'Advanced analytics',
|
|
95
|
+
],
|
|
96
|
+
popular: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'enterprise',
|
|
100
|
+
name: 'Enterprise',
|
|
101
|
+
description: 'For large teams with custom needs',
|
|
102
|
+
price: '$99/mo',
|
|
103
|
+
priceId: process.env.PLAN_ENTERPRISE_PRICE_ID ?? 'price_enterprise',
|
|
104
|
+
interval: 'month',
|
|
105
|
+
features: [
|
|
106
|
+
'Everything in Pro',
|
|
107
|
+
'Dedicated support',
|
|
108
|
+
'Custom integrations',
|
|
109
|
+
'SLA guarantee',
|
|
110
|
+
'SSO & advanced security',
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get a plan by its ID
|
|
117
|
+
*/
|
|
118
|
+
export const getPlanById = (planId: string): Plan | undefined => {
|
|
119
|
+
return PLANS.find((plan) => plan.id === planId);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get a plan by its provider-specific price ID
|
|
124
|
+
*/
|
|
125
|
+
export const getPlanByPriceId = (priceId: string): Plan | undefined => {
|
|
126
|
+
return PLANS.find((plan) => plan.priceId === priceId);
|
|
127
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Functions Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This file exports the active payment provider's functions.
|
|
5
|
+
* To switch providers, change the import below from './providers/stripe'
|
|
6
|
+
* to './providers/lemonsqueezy'. CLI tools modify this import line.
|
|
7
|
+
*
|
|
8
|
+
* All providers export the same interface:
|
|
9
|
+
* - createCheckout: Create a checkout session
|
|
10
|
+
* - createPortalSession: Create billing portal session
|
|
11
|
+
* - getSubscription: Get user's active subscription
|
|
12
|
+
* - cancelSubscription: Cancel a subscription
|
|
13
|
+
* - getPayments: Get payment history
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Provider Selection
|
|
18
|
+
// CLI tools modify this line to switch providers.
|
|
19
|
+
// Options: './providers/stripe' or './providers/lemonsqueezy'
|
|
20
|
+
// ============================================================================
|
|
21
|
+
export type PaymentProvider = 'stripe' | 'lemonsqueezy';
|
|
22
|
+
|
|
23
|
+
// ACTIVE_PROVIDER: Change to 'lemonsqueezy' when switching providers
|
|
24
|
+
export const ACTIVE_PROVIDER: PaymentProvider = 'stripe';
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Provider Exports
|
|
28
|
+
// Change this import to switch providers:
|
|
29
|
+
// - './providers/stripe' for Stripe
|
|
30
|
+
// - './providers/lemonsqueezy' for LemonSqueezy
|
|
31
|
+
// ============================================================================
|
|
32
|
+
export {
|
|
33
|
+
createCheckout,
|
|
34
|
+
createPortalSession,
|
|
35
|
+
getSubscription,
|
|
36
|
+
cancelSubscription,
|
|
37
|
+
getPayments,
|
|
38
|
+
} from './providers/stripe';
|
|
39
|
+
|
|
40
|
+
// Re-export shared config and types for frontend use
|
|
41
|
+
export {
|
|
42
|
+
PLANS,
|
|
43
|
+
getPlanById,
|
|
44
|
+
getPlanByPriceId,
|
|
45
|
+
getPaymentProvider,
|
|
46
|
+
} from './config';
|
|
47
|
+
export type {
|
|
48
|
+
Plan,
|
|
49
|
+
Subscription,
|
|
50
|
+
SubscriptionStatus,
|
|
51
|
+
BillingInterval,
|
|
52
|
+
} from './types';
|