Skip to content

Commit 7eccfce

Browse files
GeneAIclaude
authored andcommitted
feat: Add Stripe checkout integration
- Add checkout API route for creating Stripe sessions - Add webhook handler for payment events - Add customer portal API route - Add CheckoutButton component for book and license purchases - Add success page for post-payment redirect - Add contribute page with sponsorship tiers - Update book page with $49 pre-order button - Update pricing page with $99 license purchase button - Update .env.example with Stripe configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 193bb01 commit 7eccfce

File tree

9 files changed

+653
-12
lines changed

9 files changed

+653
-12
lines changed

website/.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ NEXT_PUBLIC_PLAUSIBLE_DOMAIN=smartaimemory.com
2929
# Database (if needed for newsletter/contact storage)
3030
# DATABASE_URL=postgresql://user:password@localhost:5432/smartaimemory
3131

32+
# Stripe Configuration
33+
# Get keys from: https://dashboard.stripe.com/apikeys
34+
STRIPE_SECRET_KEY=sk_live_...
35+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
36+
# Get from: Developers → Webhooks → [Your endpoint] → Signing secret
37+
STRIPE_WEBHOOK_SECRET=whsec_...
38+
39+
# Stripe Product Price IDs (get from Dashboard after creating products)
40+
# These need NEXT_PUBLIC_ prefix to be accessible in client components
41+
NEXT_PUBLIC_STRIPE_PRICE_BOOK=price_...
42+
NEXT_PUBLIC_STRIPE_PRICE_LICENSE=price_...
43+
# Contribution tiers (optional)
44+
NEXT_PUBLIC_STRIPE_PRICE_CONTRIB_5=price_...
45+
NEXT_PUBLIC_STRIPE_PRICE_CONTRIB_25=price_...
46+
NEXT_PUBLIC_STRIPE_PRICE_CONTRIB_100=price_...
47+
NEXT_PUBLIC_STRIPE_PRICE_CONTRIB_500=price_...
48+
3249
# Deployment
3350
NODE_ENV=production
3451
NEXT_PUBLIC_SITE_URL=https://smartaimemory.com
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import Stripe from 'stripe';
3+
4+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
5+
6+
export async function POST(req: NextRequest) {
7+
try {
8+
const { priceId, mode, customerEmail, successUrl, cancelUrl } = await req.json();
9+
10+
if (!priceId) {
11+
return NextResponse.json({ error: 'Price ID is required' }, { status: 400 });
12+
}
13+
14+
const session = await stripe.checkout.sessions.create({
15+
mode: mode || 'payment', // 'payment' for one-time, 'subscription' for recurring
16+
payment_method_types: ['card'],
17+
line_items: [
18+
{
19+
price: priceId,
20+
quantity: 1,
21+
},
22+
],
23+
success_url: successUrl || `${process.env.NEXT_PUBLIC_SITE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
24+
cancel_url: cancelUrl || `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
25+
customer_email: customerEmail || undefined,
26+
allow_promotion_codes: true,
27+
billing_address_collection: 'required',
28+
// Collect customer info for fulfillment
29+
customer_creation: mode === 'subscription' ? undefined : 'always',
30+
});
31+
32+
return NextResponse.json({ url: session.url, sessionId: session.id });
33+
} catch (error) {
34+
console.error('Stripe checkout error:', error);
35+
const errorMessage = error instanceof Error ? error.message : 'Checkout failed';
36+
return NextResponse.json({ error: errorMessage }, { status: 500 });
37+
}
38+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import Stripe from 'stripe';
3+
4+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
5+
6+
export async function POST(req: NextRequest) {
7+
try {
8+
const { customerId } = await req.json();
9+
10+
if (!customerId) {
11+
return NextResponse.json({ error: 'Customer ID is required' }, { status: 400 });
12+
}
13+
14+
const session = await stripe.billingPortal.sessions.create({
15+
customer: customerId,
16+
return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/account`,
17+
});
18+
19+
return NextResponse.json({ url: session.url });
20+
} catch (error) {
21+
console.error('Stripe portal error:', error);
22+
const errorMessage = error instanceof Error ? error.message : 'Portal session failed';
23+
return NextResponse.json({ error: errorMessage }, { status: 500 });
24+
}
25+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import Stripe from 'stripe';
3+
4+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
5+
6+
// Disable body parsing - we need the raw body for signature verification
7+
export const runtime = 'nodejs';
8+
9+
export async function POST(req: NextRequest) {
10+
const body = await req.text();
11+
const sig = req.headers.get('stripe-signature');
12+
13+
if (!sig) {
14+
console.error('Missing stripe-signature header');
15+
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
16+
}
17+
18+
let event: Stripe.Event;
19+
20+
try {
21+
event = stripe.webhooks.constructEvent(
22+
body,
23+
sig,
24+
process.env.STRIPE_WEBHOOK_SECRET!
25+
);
26+
} catch (err) {
27+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
28+
console.error('Webhook signature verification failed:', errorMessage);
29+
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
30+
}
31+
32+
// Handle the event
33+
try {
34+
switch (event.type) {
35+
case 'checkout.session.completed': {
36+
const session = event.data.object as Stripe.Checkout.Session;
37+
console.log('Payment successful:', {
38+
sessionId: session.id,
39+
customerEmail: session.customer_email,
40+
amountTotal: session.amount_total,
41+
paymentStatus: session.payment_status,
42+
});
43+
44+
// TODO: Implement fulfillment logic
45+
// - For book: Send download link email
46+
// - For license: Generate and send license key
47+
// - For contribution: Send thank you email
48+
49+
// Example: You could call an internal API or send an email here
50+
// await sendConfirmationEmail(session.customer_email, session.id);
51+
// await generateLicenseKey(session.customer_email);
52+
53+
break;
54+
}
55+
56+
case 'customer.subscription.created': {
57+
const subscription = event.data.object as Stripe.Subscription;
58+
console.log('Subscription created:', {
59+
subscriptionId: subscription.id,
60+
customerId: subscription.customer,
61+
status: subscription.status,
62+
});
63+
// TODO: Activate license for customer
64+
break;
65+
}
66+
67+
case 'customer.subscription.updated': {
68+
const subscription = event.data.object as Stripe.Subscription;
69+
console.log('Subscription updated:', {
70+
subscriptionId: subscription.id,
71+
status: subscription.status,
72+
});
73+
// TODO: Update license status
74+
break;
75+
}
76+
77+
case 'customer.subscription.deleted': {
78+
const subscription = event.data.object as Stripe.Subscription;
79+
console.log('Subscription cancelled:', {
80+
subscriptionId: subscription.id,
81+
customerId: subscription.customer,
82+
});
83+
// TODO: Deactivate license
84+
break;
85+
}
86+
87+
case 'invoice.paid': {
88+
const invoice = event.data.object as Stripe.Invoice;
89+
console.log('Invoice paid:', {
90+
invoiceId: invoice.id,
91+
customerId: invoice.customer,
92+
amountPaid: invoice.amount_paid,
93+
});
94+
// TODO: Extend license period
95+
break;
96+
}
97+
98+
case 'invoice.payment_failed': {
99+
const invoice = event.data.object as Stripe.Invoice;
100+
console.log('Invoice payment failed:', {
101+
invoiceId: invoice.id,
102+
customerId: invoice.customer,
103+
});
104+
// TODO: Send payment failed notification
105+
break;
106+
}
107+
108+
default:
109+
console.log(`Unhandled event type: ${event.type}`);
110+
}
111+
112+
return NextResponse.json({ received: true });
113+
} catch (error) {
114+
console.error('Error processing webhook:', error);
115+
return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 });
116+
}
117+
}

website/app/book/page.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import Link from 'next/link';
2+
import CheckoutButton from '@/components/CheckoutButton';
3+
4+
// Price ID from Stripe Dashboard - update after creating product
5+
const BOOK_PRICE_ID = process.env.NEXT_PUBLIC_STRIPE_PRICE_BOOK || 'price_book_placeholder';
26

37
export default function BookPage() {
48
return (
@@ -82,12 +86,15 @@ export default function BookPage() {
8286
</div>
8387
</div>
8488

85-
<button className="btn btn-primary w-full text-lg mb-4" disabled>
86-
Pre-order for December 2025
87-
</button>
89+
<CheckoutButton
90+
priceId={BOOK_PRICE_ID}
91+
mode="payment"
92+
buttonText="Pre-order Now - $49"
93+
className="btn btn-primary w-full text-lg mb-4"
94+
/>
8895

8996
<p className="text-xs text-center text-[var(--muted)]">
90-
Available December 2025. Pre-orders opening soon.
97+
Secure checkout powered by Stripe. Available December 2025.
9198
</p>
9299
</div>
93100
</div>
@@ -282,9 +289,12 @@ export default function BookPage() {
282289
<p className="text-xl mb-8 opacity-90">
283290
The complete guide to building Level 4 Anticipatory AI systems with working commercial plugins.
284291
</p>
285-
<button className="btn bg-white text-[var(--primary)] hover:bg-gray-100 text-lg px-8 py-4" disabled>
286-
Pre-orders Opening Soon
287-
</button>
292+
<CheckoutButton
293+
priceId={BOOK_PRICE_ID}
294+
mode="payment"
295+
buttonText="Pre-order Now - $49"
296+
className="btn bg-white text-[var(--primary)] hover:bg-gray-100 text-lg px-8 py-4"
297+
/>
288298
</div>
289299
</div>
290300
</section>

0 commit comments

Comments
 (0)