Skip to content

Commit 8822f97

Browse files
authoredNov 13, 2024··
stripe (#117)
* Trigger Build * migration * modify * npm install @stripe/stripe-js * deps * wip * wip * lint * lint * lint * try * lint * edit
1 parent 9f14c90 commit 8822f97

File tree

12 files changed

+323
-4
lines changed

12 files changed

+323
-4
lines changed
 

‎web/app/(api)/summarize/route.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function GET(request: NextRequest) {
6262
.limit(1)
6363
.maybeSingle();
6464

65-
if (summaryError) throw summaryError;
65+
// if (summaryError) throw summaryError;
6666

6767
// Return existing summary if found
6868
if (existingSummary) {

‎web/app/(landing)/LandingBody.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ export default function LandingBody({ examples }: { examples: Example[] }) {
3737
);
3838
const [thumbnailTitle, setThumbnailTitle] = useState('');
3939

40-
const inputRef = useFocusShortcut('/');
41-
4240
useEffect(() => {
4341
const showExamplesStored = localStorage.getItem('showExamples');
4442
const saveHistoryStored = localStorage.getItem('saveHistory');
@@ -167,7 +165,6 @@ export default function LandingBody({ examples }: { examples: Example[] }) {
167165
placeholder='Enter YouTube URL e.g. https://www.youtube.com/watch?v=62wEk02YKs0&pp=ygUIYmJjIG5ld3M%3D'
168166
value={url}
169167
onChange={handleInputChange}
170-
ref={inputRef}
171168
className='mb-4'
172169
/>
173170
<Button type='submit' disabled={loading} className='relative'>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import { useSearchParams } from 'next/navigation';
4+
5+
export const PricingBody = () => {
6+
const searchParams = useSearchParams();
7+
8+
const status = searchParams.get('status');
9+
return (
10+
<div className='mx-auto mb-4 w-full max-w-lg px-4'>
11+
{status === 'success' && (
12+
<div className='mb-4 rounded-lg border border-green-200 bg-green-100 p-4 text-center text-green-800'>
13+
🎉 Payment successful! Thank you for subscribing.
14+
</div>
15+
)}
16+
{status === 'cancel' && (
17+
<div className='mb-4 rounded-lg border border-red-200 bg-red-100 p-4 text-center text-red-800'>
18+
Payment was canceled. Please try again if you&apos;d like to subscribe.
19+
</div>
20+
)}
21+
</div>
22+
);
23+
};

‎web/app/(marketing)/pricing/page.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Suspense } from 'react';
2+
3+
import { PricingBody } from '@/app/(marketing)/pricing/PricingBody';
4+
import CheckoutButton from '@/components/CheckoutButton';
5+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
6+
7+
export default function Pricing() {
8+
return (
9+
<div className='flex min-h-screen flex-col items-center justify-center bg-gray-100'>
10+
<Suspense fallback={null}>
11+
<PricingBody />
12+
</Suspense>
13+
<Card className='mx-auto w-full max-w-lg rounded-lg bg-white p-6 shadow-lg'>
14+
<CardHeader>
15+
<CardTitle className='text-center text-2xl font-semibold'>
16+
Premium Plan
17+
</CardTitle>
18+
</CardHeader>
19+
<CardContent>
20+
<p className='mb-4 text-center text-gray-600'>
21+
Unlock exclusive features and support by subscribing to our premium
22+
plan.
23+
</p>
24+
<div className='flex justify-center'>
25+
<CheckoutButton priceId={process.env.NEXT_PUBLIC_TEST_ID} />
26+
</div>
27+
</CardContent>
28+
</Card>
29+
</div>
30+
);
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Stripe from 'stripe';
2+
3+
export async function POST(request) {
4+
const { priceId } = await request.json();
5+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string);
6+
7+
try {
8+
const session = await stripe.checkout.sessions.create({
9+
payment_method_types: ['card'],
10+
mode: 'subscription',
11+
line_items: [
12+
{
13+
price: priceId,
14+
quantity: 1,
15+
},
16+
],
17+
success_url: `${request.headers.get('origin')}/pricing?status=success`,
18+
cancel_url: `${request.headers.get('origin')}/pricing?status=cancel`,
19+
});
20+
21+
return new Response(JSON.stringify({ sessionId: session.id }), {
22+
status: 200,
23+
headers: { 'Content-Type': 'application/json' },
24+
});
25+
} catch (error: any) {
26+
console.log(error);
27+
return new Response(JSON.stringify({ error: error.message }), {
28+
status: 500,
29+
headers: { 'Content-Type': 'application/json' },
30+
});
31+
}
32+
}

‎web/app/api/stripe-webhook/route.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// app/api/stripe-webhook/route.ts
2+
import Stripe from 'stripe';
3+
4+
// import {
5+
// deletePriceRecord,
6+
// deleteProductRecord,
7+
// manageSubscriptionStatusChange,
8+
// upsertPriceRecord,
9+
// upsertProductRecord,
10+
// } from '@/utils/supabase/admin';
11+
12+
const relevantEvents = new Set([
13+
'product.created',
14+
'product.updated',
15+
'product.deleted',
16+
'price.created',
17+
'price.updated',
18+
'price.deleted',
19+
'checkout.session.completed',
20+
'customer.subscription.created',
21+
'customer.subscription.updated',
22+
'customer.subscription.deleted',
23+
'charge.succeeded',
24+
'payment_intent.created',
25+
'payment_intent.succeeded',
26+
]);
27+
28+
export async function POST(req: Request) {
29+
const body = await req.text();
30+
const sig = req.headers.get('stripe-signature') as string;
31+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
32+
let event: Stripe.Event;
33+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string);
34+
35+
try {
36+
if (!sig || !webhookSecret)
37+
return new Response('Webhook secret not found.', { status: 400 });
38+
event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
39+
console.log(`🔔 Webhook received: ${event.type}`);
40+
} catch (err: any) {
41+
console.log(`❌ Error message: ${err.message}`);
42+
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
43+
}
44+
45+
if (relevantEvents.has(event.type)) {
46+
try {
47+
switch (event.type) {
48+
case 'product.created':
49+
case 'product.updated':
50+
// await upsertProductRecord(event.data.object as Stripe.Product);
51+
break;
52+
case 'price.created':
53+
case 'price.updated':
54+
// await upsertPriceRecord(event.data.object as Stripe.Price);
55+
break;
56+
case 'price.deleted':
57+
// await deletePriceRecord(event.data.object as Stripe.Price);
58+
break;
59+
case 'product.deleted':
60+
// await deleteProductRecord(event.data.object as Stripe.Product);
61+
break;
62+
case 'customer.subscription.created':
63+
case 'customer.subscription.updated':
64+
case 'customer.subscription.deleted':
65+
// const subscription = event.data.object as Stripe.Subscription;
66+
// await manageSubscriptionStatusChange(
67+
// subscription.id,
68+
// subscription.customer as string,
69+
// event.type === 'customer.subscription.created'
70+
// );
71+
break;
72+
case 'checkout.session.completed':
73+
console.log('checkout.session.completed');
74+
// const checkoutSession = event.data.object as Stripe.Checkout.Session;
75+
// if (checkoutSession.mode === 'subscription') {
76+
// const subscriptionId = checkoutSession.subscription;
77+
// await manageSubscriptionStatusChange(
78+
// subscriptionId as string,
79+
// checkoutSession.customer as string,
80+
// true
81+
// );
82+
// }
83+
break;
84+
case 'payment_intent.created':
85+
break;
86+
case 'payment_intent.succeeded':
87+
break;
88+
case 'charge.succeeded':
89+
break;
90+
default:
91+
throw new Error('Unhandled relevant event!');
92+
}
93+
} catch (error) {
94+
console.log(error);
95+
return new Response(
96+
'Webhook handler failed. View your Next.js function logs.',
97+
{
98+
status: 400,
99+
}
100+
);
101+
}
102+
} else {
103+
return new Response(`Unsupported event type: ${event.type}`, {
104+
status: 400,
105+
});
106+
}
107+
return new Response(JSON.stringify({ received: true }));
108+
}

‎web/components/CheckoutButton.tsx

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client';
2+
3+
import { loadStripe } from '@stripe/stripe-js';
4+
import { Stripe } from '@stripe/stripe-js';
5+
import { Loader2 } from 'lucide-react';
6+
import { useState } from 'react';
7+
8+
import { Button } from '@/components/ui/button';
9+
10+
const stripePromise = loadStripe(
11+
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string
12+
) as Promise<Stripe>;
13+
14+
export default function CheckoutButton({ priceId }) {
15+
const [loading, setLoading] = useState(false);
16+
17+
const handleCheckout = async () => {
18+
setLoading(true);
19+
20+
const response = await fetch('/api/create-checkout-session', {
21+
method: 'POST',
22+
headers: {
23+
'Content-Type': 'application/json',
24+
},
25+
body: JSON.stringify({ priceId }),
26+
});
27+
28+
const { sessionId } = await response.json();
29+
const stripe = await stripePromise;
30+
await stripe.redirectToCheckout({ sessionId });
31+
32+
setLoading(false);
33+
};
34+
35+
return (
36+
<Button onClick={handleCheckout} disabled={loading} className='w-full'>
37+
{loading ? <Loader2 className='animate-spin' /> : 'Subscribe Now'}
38+
</Button>
39+
);
40+
}

‎web/components/layout/Nav.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ export const Nav = async () => {
7575
</Link>
7676
</SheetClose>
7777
)}
78+
<SheetClose asChild>
79+
<Link
80+
href={AppConfig.SITE_MAP.PRICING}
81+
className='flex items-center py-2 text-lg font-semibold'
82+
>
83+
Pricing
84+
</Link>
85+
</SheetClose>
7886
<SheetClose asChild>
7987
<Link
8088
href={AppConfig.SITE_MAP.BLOG}
@@ -97,6 +105,12 @@ export const Nav = async () => {
97105
History
98106
</Link>
99107
)}
108+
<Link
109+
href={AppConfig.SITE_MAP.PRICING}
110+
className='flex items-center text-sm font-medium transition-colors hover:underline'
111+
>
112+
Pricing
113+
</Link>
100114
<Link
101115
href={AppConfig.SITE_MAP.BLOG}
102116
className='flex items-center text-sm font-medium transition-colors hover:underline'

‎web/db/20241110150742_add_stripe.sql

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
-- Minimal Users table
2+
CREATE TABLE public.users (
3+
id UUID REFERENCES auth.users(id) PRIMARY KEY,
4+
full_name TEXT,
5+
avatar_url TEXT
6+
);
7+
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
8+
CREATE POLICY "Can view own data" ON public.users FOR SELECT USING (auth.uid() = id);
9+
CREATE POLICY "Can update own data" ON public.users FOR UPDATE USING (auth.uid() = id);
10+
11+
-- Customers table for Stripe customer ID
12+
CREATE TABLE public.customers (
13+
id UUID REFERENCES auth.users(id) PRIMARY KEY,
14+
stripe_customer_id TEXT
15+
);
16+
17+
-- Minimal Products table
18+
CREATE TABLE public.products (
19+
id TEXT PRIMARY KEY,
20+
active BOOLEAN,
21+
name TEXT
22+
);
23+
ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
24+
CREATE POLICY "Allow public read-only access" ON public.products FOR SELECT USING (TRUE);
25+
26+
-- Minimal Prices table
27+
CREATE TABLE public.prices (
28+
id TEXT PRIMARY KEY,
29+
product_id TEXT REFERENCES public.products,
30+
active BOOLEAN,
31+
unit_amount BIGINT,
32+
currency TEXT CHECK (char_length(currency) = 3)
33+
);
34+
ALTER TABLE public.prices ENABLE ROW LEVEL SECURITY;
35+
CREATE POLICY "Allow public read-only access" ON public.prices FOR SELECT USING (TRUE);
36+
37+
-- Minimal Subscriptions table
38+
CREATE TABLE public.subscriptions (
39+
id TEXT PRIMARY KEY,
40+
user_id UUID REFERENCES auth.users(id) NOT NULL,
41+
status TEXT,
42+
price_id TEXT REFERENCES public.prices
43+
);
44+
ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
45+
CREATE POLICY "Can view own subs data" ON public.subscriptions FOR SELECT USING (auth.uid() = user_id);
46+
47+
-- Minimal real-time publication for products and prices
48+
-- DROP PUBLICATION IF EXISTS supabase_realtime;
49+
-- CREATE PUBLICATION supabase_realtime FOR TABLE public.products, public.prices;

‎web/lib/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const AppConfig = {
1313
TERMS: '/policies/terms',
1414
HISTORY: '/history',
1515
BLOG: '/blog',
16+
PRICING: '/pricing',
1617
},
1718
SOCIAL: {
1819
GITHUB: 'https://github.com/adnjoo/summatube',

‎web/package-lock.json

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎web/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@radix-ui/react-separator": "^1.1.0",
1515
"@radix-ui/react-slot": "^1.0.2",
1616
"@radix-ui/react-switch": "^1.0.3",
17+
"@stripe/stripe-js": "^4.10.0",
1718
"@supabase/ssr": "^0.5.1",
1819
"@supabase/supabase-js": "^2.46.1",
1920
"@tailwindcss/typography": "^0.5.15",
@@ -35,6 +36,7 @@
3536
"rehype-autolink-headings": "^7.1.0",
3637
"rehype-slug": "^6.0.0",
3738
"remark-gfm": "^4.0.0",
39+
"stripe": "^17.3.1",
3840
"tailwind-merge": "^2.3.0",
3941
"tailwindcss-animate": "^1.0.7",
4042
"youtubei.js": "^10.3.0"

0 commit comments

Comments
 (0)
Please sign in to comment.