vibefast-cli 1.3.3 → 1.3.5
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 +338 -6
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +33 -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-supabase/apps/web/app/billing/page.tsx +48 -0
- package/recipes/lemonsqueezy-supabase/apps/web/app/pricing/page.tsx +19 -0
- package/recipes/lemonsqueezy-supabase/apps/web/components/payments/BillingSection.tsx +224 -0
- package/recipes/lemonsqueezy-supabase/apps/web/components/payments/PricingCard.tsx +87 -0
- package/recipes/lemonsqueezy-supabase/apps/web/components/payments/PricingSection.tsx +100 -0
- package/recipes/lemonsqueezy-supabase/apps/web/components/payments/index.ts +2 -0
- package/recipes/lemonsqueezy-supabase/apps/web/lib/plans.ts +84 -0
- package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/config.ts +92 -0
- package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/index.ts +138 -0
- package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/providers/lemonsqueezy.ts +165 -0
- package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/providers/stripe.ts +179 -0
- package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments/types.ts +57 -0
- package/recipes/lemonsqueezy-supabase/packages/backend/supabase/functions/payments-webhook/index.ts +339 -0
- package/recipes/lemonsqueezy-supabase/packages/backend/supabase/migrations/web_payments.sql +85 -0
- package/recipes/lemonsqueezy-supabase/recipe.json +129 -0
- package/recipes/lemonsqueezy-supabase-web@latest.zip +0 -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-supabase/apps/web/app/billing/page.tsx +48 -0
- package/recipes/stripe-supabase/apps/web/app/pricing/page.tsx +19 -0
- package/recipes/stripe-supabase/apps/web/components/payments/BillingSection.tsx +224 -0
- package/recipes/stripe-supabase/apps/web/components/payments/PricingCard.tsx +87 -0
- package/recipes/stripe-supabase/apps/web/components/payments/PricingSection.tsx +100 -0
- package/recipes/stripe-supabase/apps/web/components/payments/index.ts +2 -0
- package/recipes/stripe-supabase/apps/web/lib/plans.ts +84 -0
- package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/config.ts +92 -0
- package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/index.ts +138 -0
- package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/providers/lemonsqueezy.ts +165 -0
- package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/providers/stripe.ts +179 -0
- package/recipes/stripe-supabase/packages/backend/supabase/functions/payments/types.ts +57 -0
- package/recipes/stripe-supabase/packages/backend/supabase/functions/payments-webhook/index.ts +339 -0
- package/recipes/stripe-supabase/packages/backend/supabase/migrations/web_payments.sql +85 -0
- package/recipes/stripe-supabase/recipe.json +123 -0
- package/recipes/stripe-supabase-web@latest.zip +0 -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,186 @@
|
|
|
1
|
+
import { action, query } from '../../_generated/server';
|
|
2
|
+
import { components } from '../../_generated/api';
|
|
3
|
+
import { StripeSubscriptions } from '@convex-dev/stripe';
|
|
4
|
+
import { v } from 'convex/values';
|
|
5
|
+
import {
|
|
6
|
+
checkoutResultValidator,
|
|
7
|
+
portalResultValidator,
|
|
8
|
+
subscriptionValidator,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import { getCheckoutUrls, getPlanByPriceId } from '../config';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Stripe Payment Provider Implementation
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
const stripe = new StripeSubscriptions(components.stripe, {});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a checkout session for subscription or one-time payment
|
|
20
|
+
*/
|
|
21
|
+
export const createCheckout = action({
|
|
22
|
+
args: {
|
|
23
|
+
priceId: v.string(),
|
|
24
|
+
mode: v.optional(v.union(v.literal('subscription'), v.literal('payment'))),
|
|
25
|
+
successUrl: v.optional(v.string()),
|
|
26
|
+
cancelUrl: v.optional(v.string()),
|
|
27
|
+
},
|
|
28
|
+
returns: checkoutResultValidator,
|
|
29
|
+
handler: async (ctx, args) => {
|
|
30
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
31
|
+
if (!identity) {
|
|
32
|
+
throw new Error('Not authenticated');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const userId = identity.subject;
|
|
36
|
+
const urls = getCheckoutUrls(args.successUrl, args.cancelUrl);
|
|
37
|
+
|
|
38
|
+
// Get or create a Stripe customer linked to this user
|
|
39
|
+
const customer = await stripe.getOrCreateCustomer(ctx, {
|
|
40
|
+
userId,
|
|
41
|
+
email: identity.email,
|
|
42
|
+
name: identity.name,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Create checkout session
|
|
46
|
+
const session = await stripe.createCheckoutSession(ctx, {
|
|
47
|
+
priceId: args.priceId,
|
|
48
|
+
customerId: customer.customerId,
|
|
49
|
+
mode: args.mode ?? 'subscription',
|
|
50
|
+
successUrl: urls.successUrl,
|
|
51
|
+
cancelUrl: urls.cancelUrl,
|
|
52
|
+
subscriptionMetadata: { userId },
|
|
53
|
+
paymentIntentMetadata: { userId },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
url: session.url ?? '',
|
|
58
|
+
sessionId: session.sessionId,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a customer portal session for billing management
|
|
65
|
+
*/
|
|
66
|
+
export const createPortalSession = action({
|
|
67
|
+
args: {
|
|
68
|
+
returnUrl: v.optional(v.string()),
|
|
69
|
+
},
|
|
70
|
+
returns: portalResultValidator,
|
|
71
|
+
handler: async (ctx, args) => {
|
|
72
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
73
|
+
if (!identity) {
|
|
74
|
+
throw new Error('Not authenticated');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const customer = await stripe.getOrCreateCustomer(ctx, {
|
|
78
|
+
userId: identity.subject,
|
|
79
|
+
email: identity.email,
|
|
80
|
+
name: identity.name,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = await stripe.createCustomerPortalSession(ctx, {
|
|
84
|
+
customerId: customer.customerId,
|
|
85
|
+
returnUrl: args.returnUrl ?? `${process.env.SITE_URL}/billing`,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { url: result.url };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the current user's active subscription
|
|
94
|
+
*/
|
|
95
|
+
export const getSubscription = query({
|
|
96
|
+
args: {},
|
|
97
|
+
returns: v.union(subscriptionValidator, v.null()),
|
|
98
|
+
handler: async (ctx) => {
|
|
99
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
100
|
+
if (!identity) return null;
|
|
101
|
+
|
|
102
|
+
const subscriptions = await ctx.runQuery(
|
|
103
|
+
components.stripe.public.listSubscriptionsByUserId,
|
|
104
|
+
{ userId: identity.subject },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Find active or trialing subscription
|
|
108
|
+
const active = subscriptions.find(
|
|
109
|
+
(s) => s.status === 'active' || s.status === 'trialing',
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (!active) return null;
|
|
113
|
+
|
|
114
|
+
// Map to unified subscription shape
|
|
115
|
+
const plan = getPlanByPriceId(active.priceId ?? '');
|
|
116
|
+
const cancelAtRaw = (active as any)?.metadata?.cancelAt;
|
|
117
|
+
const cancelAt =
|
|
118
|
+
typeof cancelAtRaw === 'number'
|
|
119
|
+
? cancelAtRaw
|
|
120
|
+
: typeof cancelAtRaw === 'string'
|
|
121
|
+
? Number(cancelAtRaw)
|
|
122
|
+
: undefined;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
id: active.stripeSubscriptionId,
|
|
126
|
+
provider: 'stripe',
|
|
127
|
+
status: active.status,
|
|
128
|
+
planId: plan?.id ?? '',
|
|
129
|
+
priceId: active.priceId ?? undefined,
|
|
130
|
+
currentPeriodEnd: active.currentPeriodEnd,
|
|
131
|
+
cancelAt: Number.isFinite(cancelAt) ? cancelAt : undefined,
|
|
132
|
+
cancelAtPeriodEnd: active.cancelAtPeriodEnd,
|
|
133
|
+
createdAt: active._creationTime,
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Cancel a subscription (at period end)
|
|
140
|
+
*/
|
|
141
|
+
export const cancelSubscription = action({
|
|
142
|
+
args: { subscriptionId: v.string() },
|
|
143
|
+
returns: v.boolean(),
|
|
144
|
+
handler: async (ctx, args) => {
|
|
145
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
146
|
+
if (!identity) {
|
|
147
|
+
throw new Error('Not authenticated');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await stripe.cancelSubscription(ctx, {
|
|
151
|
+
stripeSubscriptionId: args.subscriptionId,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return true;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get user's payment history
|
|
160
|
+
*/
|
|
161
|
+
export const getPayments = query({
|
|
162
|
+
args: {},
|
|
163
|
+
handler: async (ctx) => {
|
|
164
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
165
|
+
if (!identity) return [];
|
|
166
|
+
|
|
167
|
+
return await ctx.runQuery(components.stripe.public.listPaymentsByUserId, {
|
|
168
|
+
userId: identity.subject,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get user's invoices
|
|
175
|
+
*/
|
|
176
|
+
export const getInvoices = query({
|
|
177
|
+
args: {},
|
|
178
|
+
handler: async (ctx) => {
|
|
179
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
180
|
+
if (!identity) return [];
|
|
181
|
+
|
|
182
|
+
return await ctx.runQuery(components.stripe.public.listInvoicesByUserId, {
|
|
183
|
+
userId: identity.subject,
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { v } from 'convex/values';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Payment Provider Types
|
|
5
|
+
// Shared interfaces for all payment providers (Stripe, LemonSqueezy, etc.)
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalized subscription status across all providers
|
|
10
|
+
*/
|
|
11
|
+
export type SubscriptionStatus =
|
|
12
|
+
| 'active'
|
|
13
|
+
| 'trialing'
|
|
14
|
+
| 'past_due'
|
|
15
|
+
| 'canceled'
|
|
16
|
+
| 'expired'
|
|
17
|
+
| 'paused'
|
|
18
|
+
| 'unpaid';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Billing interval types
|
|
22
|
+
*/
|
|
23
|
+
export type BillingInterval = 'month' | 'year' | 'one_time';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Plan definition for pricing display
|
|
27
|
+
*/
|
|
28
|
+
export interface Plan {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
price: string; // Display price (e.g., "$29/mo")
|
|
33
|
+
priceId: string; // Provider-specific price/variant ID
|
|
34
|
+
interval: BillingInterval;
|
|
35
|
+
features: string[];
|
|
36
|
+
popular?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Convex validators
|
|
40
|
+
export const planValidator = v.object({
|
|
41
|
+
id: v.string(),
|
|
42
|
+
name: v.string(),
|
|
43
|
+
description: v.optional(v.string()),
|
|
44
|
+
price: v.string(),
|
|
45
|
+
priceId: v.string(),
|
|
46
|
+
interval: v.union(
|
|
47
|
+
v.literal('month'),
|
|
48
|
+
v.literal('year'),
|
|
49
|
+
v.literal('one_time'),
|
|
50
|
+
),
|
|
51
|
+
features: v.array(v.string()),
|
|
52
|
+
popular: v.optional(v.boolean()),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Unified subscription shape returned by all providers
|
|
57
|
+
*/
|
|
58
|
+
export const subscriptionValidator = v.object({
|
|
59
|
+
id: v.string(),
|
|
60
|
+
provider: v.string(),
|
|
61
|
+
status: v.string(),
|
|
62
|
+
planId: v.string(),
|
|
63
|
+
priceId: v.optional(v.string()),
|
|
64
|
+
currentPeriodEnd: v.optional(v.number()),
|
|
65
|
+
cancelAt: v.optional(v.number()),
|
|
66
|
+
cancelAtPeriodEnd: v.optional(v.boolean()),
|
|
67
|
+
createdAt: v.optional(v.number()),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export type Subscription = typeof subscriptionValidator._type;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Checkout session result
|
|
74
|
+
*/
|
|
75
|
+
export const checkoutResultValidator = v.object({
|
|
76
|
+
url: v.string(),
|
|
77
|
+
sessionId: v.optional(v.string()),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export type CheckoutResult = typeof checkoutResultValidator._type;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Portal session result
|
|
84
|
+
*/
|
|
85
|
+
export const portalResultValidator = v.object({
|
|
86
|
+
url: v.string(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type PortalResult = typeof portalResultValidator._type;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { getAuthUserId } from '@convex-dev/auth/server';
|
|
2
|
+
import { v } from 'convex/values';
|
|
3
|
+
|
|
4
|
+
import { mutation, query } from './_generated/server';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Payment Provider Exports
|
|
8
|
+
// All payment functions are delegated through the provider interface.
|
|
9
|
+
// Change ACTIVE_PROVIDER in payments/index.ts to switch providers.
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
createCheckout,
|
|
14
|
+
createPortalSession,
|
|
15
|
+
getSubscription,
|
|
16
|
+
cancelSubscription,
|
|
17
|
+
getPayments,
|
|
18
|
+
} from './payments/index';
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Credits & Consumable Purchases
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Record a consumable purchase (like credits) in the database
|
|
26
|
+
*/
|
|
27
|
+
export const recordConsumablePurchase = mutation({
|
|
28
|
+
args: {
|
|
29
|
+
productId: v.string(),
|
|
30
|
+
quantity: v.number(),
|
|
31
|
+
},
|
|
32
|
+
returns: v.object({
|
|
33
|
+
success: v.boolean(),
|
|
34
|
+
newCredits: v.number(),
|
|
35
|
+
totalCredits: v.number(),
|
|
36
|
+
}),
|
|
37
|
+
handler: async (ctx, args) => {
|
|
38
|
+
const userId = await getAuthUserId(ctx);
|
|
39
|
+
if (!userId) {
|
|
40
|
+
throw new Error('Not authenticated');
|
|
41
|
+
}
|
|
42
|
+
// Get current user to check existing credits
|
|
43
|
+
const user = await ctx.db.get(userId);
|
|
44
|
+
if (!user) {
|
|
45
|
+
throw new Error('User not found');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Calculate new credits based on product
|
|
49
|
+
let creditsToAdd = args.quantity;
|
|
50
|
+
|
|
51
|
+
// You can customize credit amounts based on productId
|
|
52
|
+
if (args.productId.includes('premium')) {
|
|
53
|
+
creditsToAdd = args.quantity * 2; // Premium products give 2x credits
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Update user credits - credits is optional in schema
|
|
57
|
+
const currentCredits = user.credits ?? 0;
|
|
58
|
+
const newTotalCredits = currentCredits + creditsToAdd;
|
|
59
|
+
|
|
60
|
+
await ctx.db.patch(userId, {
|
|
61
|
+
credits: newTotalCredits,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Log the purchase
|
|
65
|
+
await ctx.db.insert('purchases', {
|
|
66
|
+
userId,
|
|
67
|
+
productId: args.productId,
|
|
68
|
+
quantity: args.quantity,
|
|
69
|
+
creditsAdded: creditsToAdd,
|
|
70
|
+
purchaseDate: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
newCredits: creditsToAdd,
|
|
76
|
+
totalCredits: newTotalCredits,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get user's current credit balance
|
|
83
|
+
*/
|
|
84
|
+
export const getUserCredits = query({
|
|
85
|
+
args: {},
|
|
86
|
+
returns: v.union(v.number(), v.null()),
|
|
87
|
+
handler: async (ctx) => {
|
|
88
|
+
const userId = await getAuthUserId(ctx);
|
|
89
|
+
if (!userId) {
|
|
90
|
+
throw new Error('Not authenticated');
|
|
91
|
+
}
|
|
92
|
+
const user = await ctx.db.get(userId);
|
|
93
|
+
|
|
94
|
+
return user?.credits ?? 0;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get user's purchase history with pagination
|
|
100
|
+
*/
|
|
101
|
+
export const getPurchaseHistory = query({
|
|
102
|
+
args: {
|
|
103
|
+
paginationOpts: v.optional(
|
|
104
|
+
v.object({
|
|
105
|
+
numItems: v.number(),
|
|
106
|
+
cursor: v.union(v.string(), v.null()),
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
returns: v.object({
|
|
111
|
+
page: v.array(
|
|
112
|
+
v.object({
|
|
113
|
+
_id: v.id('purchases'),
|
|
114
|
+
_creationTime: v.number(),
|
|
115
|
+
productId: v.string(),
|
|
116
|
+
quantity: v.number(),
|
|
117
|
+
creditsAdded: v.number(),
|
|
118
|
+
purchaseDate: v.number(),
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
isDone: v.boolean(),
|
|
122
|
+
continueCursor: v.string(),
|
|
123
|
+
pageStatus: v.optional(v.union(v.string(), v.null())),
|
|
124
|
+
splitCursor: v.optional(v.union(v.string(), v.null())),
|
|
125
|
+
}),
|
|
126
|
+
handler: async (ctx, args) => {
|
|
127
|
+
const userId = await getAuthUserId(ctx);
|
|
128
|
+
if (!userId) {
|
|
129
|
+
throw new Error('Not authenticated');
|
|
130
|
+
}
|
|
131
|
+
return await ctx.db
|
|
132
|
+
.query('purchases')
|
|
133
|
+
.withIndex('by_userId', (q) => q.eq('userId', userId))
|
|
134
|
+
.order('desc')
|
|
135
|
+
.paginate(args.paginationOpts ?? { numItems: 50, cursor: null });
|
|
136
|
+
},
|
|
137
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stripe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Stripe web payments (pricing + billing)",
|
|
5
|
+
"copy": [
|
|
6
|
+
{
|
|
7
|
+
"from": "apps/web/app/pricing",
|
|
8
|
+
"to": "apps/web/app/pricing"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"from": "apps/web/app/billing",
|
|
12
|
+
"to": "apps/web/app/billing"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"from": "apps/web/components/payments",
|
|
16
|
+
"to": "apps/web/components/payments"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"from": "apps/web/lib/plans.ts",
|
|
20
|
+
"to": "apps/web/lib/plans.ts"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"from": "packages/backend/convex/payments",
|
|
24
|
+
"to": "packages/backend/convex/payments"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"from": "packages/backend/convex/payments.ts",
|
|
28
|
+
"to": "packages/backend/convex/payments.ts"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"target": "web",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"backend": [
|
|
34
|
+
"@convex-dev/stripe"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"env": [
|
|
38
|
+
{
|
|
39
|
+
"key": "PAYMENT_PROVIDER",
|
|
40
|
+
"description": "Active payments provider (stripe or lemonsqueezy)",
|
|
41
|
+
"example": "stripe",
|
|
42
|
+
"file": "packages/backend/.env.local"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"key": "SITE_URL",
|
|
46
|
+
"description": "Public site URL for checkout redirects (no trailing slash)",
|
|
47
|
+
"example": "https://yourdomain.com",
|
|
48
|
+
"file": "packages/backend/.env.local"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"key": "PLAN_STARTER_PRICE_ID",
|
|
52
|
+
"description": "Stripe Price ID for Starter plan",
|
|
53
|
+
"example": "price_1234",
|
|
54
|
+
"file": "packages/backend/.env.local"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"key": "PLAN_PRO_PRICE_ID",
|
|
58
|
+
"description": "Stripe Price ID for Pro plan",
|
|
59
|
+
"example": "price_5678",
|
|
60
|
+
"file": "packages/backend/.env.local"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"key": "PLAN_ENTERPRISE_PRICE_ID",
|
|
64
|
+
"description": "Stripe Price ID for Enterprise plan",
|
|
65
|
+
"example": "price_9012",
|
|
66
|
+
"file": "packages/backend/.env.local"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"key": "STRIPE_SECRET_KEY",
|
|
70
|
+
"description": "Stripe secret key",
|
|
71
|
+
"example": "sk_live_...",
|
|
72
|
+
"file": "packages/backend/.env.local"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"key": "STRIPE_WEBHOOK_SECRET",
|
|
76
|
+
"description": "Stripe webhook signing secret",
|
|
77
|
+
"example": "whsec_...",
|
|
78
|
+
"file": "packages/backend/.env.local"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"key": "NEXT_PUBLIC_PLAN_STARTER_PRICE_ID",
|
|
82
|
+
"description": "Starter plan Price ID for the web app",
|
|
83
|
+
"example": "price_1234",
|
|
84
|
+
"file": "apps/web/.env.local"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"key": "NEXT_PUBLIC_PLAN_PRO_PRICE_ID",
|
|
88
|
+
"description": "Pro plan Price ID for the web app",
|
|
89
|
+
"example": "price_5678",
|
|
90
|
+
"file": "apps/web/.env.local"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"key": "NEXT_PUBLIC_PLAN_ENTERPRISE_PRICE_ID",
|
|
94
|
+
"description": "Enterprise plan Price ID for the web app",
|
|
95
|
+
"example": "price_9012",
|
|
96
|
+
"file": "apps/web/.env.local"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"manualSteps": [
|
|
100
|
+
{
|
|
101
|
+
"title": "Create Stripe webhook",
|
|
102
|
+
"description": "Add a webhook endpoint in Stripe: https://<your-deployment>.convex.site/stripe/webhook",
|
|
103
|
+
"file": "Stripe Dashboard"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"title": "Update pricing plans",
|
|
107
|
+
"description": "Update plan names, prices, and feature lists in apps/web/lib/plans.ts and packages/backend/convex/payments/config.ts to match your Stripe products.",
|
|
108
|
+
"file": "apps/web/lib/plans.ts"
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"postInstall": {
|
|
112
|
+
"message": "✅ Stripe web payments added. Set env vars, configure webhooks, then run convex dev."
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Layout } from '@/components/Layout';
|
|
4
|
+
import { BillingSection } from '@/components/payments';
|
|
5
|
+
import { Authenticated, Unauthenticated } from '@/lib/supabase-provider';
|
|
6
|
+
import Link from 'next/link';
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Billing Page
|
|
11
|
+
// User's subscription and billing management
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export default function BillingPage() {
|
|
15
|
+
return (
|
|
16
|
+
<Layout>
|
|
17
|
+
<div className="container max-w-2xl py-8">
|
|
18
|
+
<div className="mb-8">
|
|
19
|
+
<h1 className="text-2xl font-bold">Billing & Subscription</h1>
|
|
20
|
+
<p className="mt-1 text-muted-foreground">
|
|
21
|
+
Manage your subscription and billing details
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<Authenticated>
|
|
26
|
+
<BillingSection />
|
|
27
|
+
</Authenticated>
|
|
28
|
+
|
|
29
|
+
<Unauthenticated>
|
|
30
|
+
<div className="rounded-lg border bg-card p-8 text-center">
|
|
31
|
+
<h2 className="text-lg font-semibold">Sign in to view billing</h2>
|
|
32
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
33
|
+
You need to be signed in to manage your subscription.
|
|
34
|
+
</p>
|
|
35
|
+
<div className="mt-6 flex justify-center gap-4">
|
|
36
|
+
<Button asChild>
|
|
37
|
+
<Link href="/">Sign In</Link>
|
|
38
|
+
</Button>
|
|
39
|
+
<Button variant="outline" asChild>
|
|
40
|
+
<Link href="/pricing">View Plans</Link>
|
|
41
|
+
</Button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</Unauthenticated>
|
|
45
|
+
</div>
|
|
46
|
+
</Layout>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Layout } from '@/components/Layout';
|
|
4
|
+
import { PricingSection } from '@/components/payments';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Pricing Page
|
|
8
|
+
// Displays available subscription plans
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export default function PricingPage() {
|
|
12
|
+
return (
|
|
13
|
+
<Layout>
|
|
14
|
+
<div className="container max-w-6xl py-8">
|
|
15
|
+
<PricingSection />
|
|
16
|
+
</div>
|
|
17
|
+
</Layout>
|
|
18
|
+
);
|
|
19
|
+
}
|