Skip to content

Commit

Permalink
Merge pull request #16 from Hombre2014/stripe
Browse files Browse the repository at this point in the history
Stripe
  • Loading branch information
Hombre2014 authored Nov 21, 2023
2 parents 12aac6c + b185879 commit cb7914f
Show file tree
Hide file tree
Showing 24 changed files with 692 additions and 32 deletions.
74 changes: 73 additions & 1 deletion app/(chat)/(routes)/chat/[chatId]/components/client.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,75 @@
// 'use client';

// import { useCompletion } from 'ai/react';
// import { FormEvent, useState } from 'react';
// import { Companion, Message } from '@prisma/client';
// import { useRouter } from 'next/navigation';

// import { ChatForm } from '@/components/chat-form';
// import { ChatHeader } from '@/components/chat-header';
// import { ChatMessages } from '@/components/chat-messages';
// import { ChatMessageProps } from '@/components/chat-message';

// interface ChatClientProps {
// companion: Companion & {
// messages: Message[];
// _count: {
// messages: number;
// };
// };
// }

// export const ChatClient = ({ companion }: ChatClientProps) => {
// const router = useRouter();
// const [messages, setMessages] = useState<ChatMessageProps[]>(
// companion.messages as ChatMessageProps[]
// );

// const { input, isLoading, handleInputChange, handleSubmit, setInput } =
// useCompletion({
// api: `/api/chat/${companion.id}`,
// onFinish(_prompt, completion) {
// const systemMessage: ChatMessageProps = {
// role: 'system',
// content: completion,
// };

// setMessages((current) => [...current, systemMessage]);
// setInput('');

// router.refresh();
// },
// });

// const onSubmit = (e: FormEvent<HTMLFormElement>) => {
// const userMessage: ChatMessageProps = {
// role: 'user',
// content: input,
// };

// setMessages((current) => [...current, userMessage]);

// handleSubmit(e);
// };

// return (
// <div className="flex flex-col h-full p-4 space-y-2">
// <ChatHeader companion={companion} />
// <ChatMessages
// companion={companion}
// isLoading={isLoading}
// messages={messages}
// />
// <ChatForm
// isLoading={isLoading}
// input={input}
// handleInputChange={handleInputChange}
// onSubmit={onSubmit}
// />
// </div>
// );
// };

'use client';

import { useCompletion } from 'ai/react';
Expand All @@ -22,7 +94,7 @@ interface ChatClientProps {
export const ChatClient = ({ companion }: ChatClientProps) => {
const router = useRouter();
const [messages, setMessages] = useState<ChatMessageProps[]>(
companion.messages
companion.messages as ChatMessageProps[]
);

const { input, isLoading, handleInputChange, handleSubmit, setInput } =
Expand Down
20 changes: 20 additions & 0 deletions app/(root)/(routes)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SubscriptionButton } from '@/components/subscription-button';
import { checkSubscription } from '@/lib/subscription';

const SettingsPage = async () => {
const isPro = await checkSubscription();

return (
<div className="h-full p-4 space-y-2">
<h3 className="text-lg font-medium">Settings</h3>
<div className="text-muted-foreground text-sm">
{isPro
? 'You are currently on a Pro plan.'
: 'You are currently on a free plan.'}
</div>
<SubscriptionButton isPro={isPro} />
</div>
);
};

export default SettingsPage;
9 changes: 6 additions & 3 deletions app/(root)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import Navbar from '@/components/navbar';
import Sidebar from '@/components/sidebar';
import { checkSubscription } from '@/lib/subscription';

const RootLayout = async ({ children }: { children: React.ReactNode }) => {
const isPro = await checkSubscription();

const RootLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="h-full">
<Navbar />
<Navbar isPro={isPro} />
<div className="hidden md:flex mt-16 w-20 flex-col fixed inset-y-0">
<Sidebar />
<Sidebar isPro={isPro} />
</div>
<main className="md:pl-20 pt-16 h-full">{children}</main>
</div>
Expand Down
9 changes: 8 additions & 1 deletion app/api/companion/[companionId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { auth, currentUser } from '@clerk/nextjs';
import { NextResponse } from 'next/server';

import prismadb from '@/lib/prismadb';
import { checkSubscription } from '@/lib/subscription';

export async function PATCH(
req: Request,
Expand Down Expand Up @@ -31,7 +32,13 @@ export async function PATCH(
return new NextResponse('Missing required fields', { status: 400 });
}

// TODO Check for subscription
const isPro = await checkSubscription();

if (!isPro) {
return new NextResponse('Please subscribe to use this feature', {
status: 403,
});
}

const companion = await prismadb.companion.update({
where: {
Expand Down
11 changes: 10 additions & 1 deletion app/api/companion/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { currentUser } from '@clerk/nextjs';
import { NextResponse } from 'next/server';

import prismadb from '@/lib/prismadb';
import { checkSubscription } from '@/lib/subscription';

export async function POST(req: Request) {
try {
const body = await req.json();
const user = await currentUser();
const { src, name, description, instructions, seed, categoryId } = body;

// console.log('User from currentUser:', user);

if (!user || !user.id || !user.firstName) {
console.log('User in API Companion route', user);
return new NextResponse('Unauthorized', { status: 401 });
Expand All @@ -25,7 +28,13 @@ export async function POST(req: Request) {
return new NextResponse('Missing required fields', { status: 400 });
}

// TODO Check for subscription
const isPro = await checkSubscription();

if (!isPro) {
return new NextResponse('Please subscribe to use this feature', {
status: 403,
});
}

const companion = await prismadb.companion.create({
data: {
Expand Down
73 changes: 73 additions & 0 deletions app/api/stripe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { auth, currentUser } from '@clerk/nextjs';
import { NextResponse } from 'next/server';

import prismadb from '@/lib/prismadb';
import { stripe } from '@/lib/stripe';
import { absoluteUrl } from '@/lib/utils';

const settingsUrl = absoluteUrl('/settings');

console.log('URL: ', settingsUrl);

export async function GET() {
try {
const { userId } = auth();
const user = await currentUser();

if (!userId || !user) {
return new NextResponse('Unauthorized', { status: 401 });
}

const userSubscription = await prismadb.userSubscription.findUnique({
where: {
userId,
},
});

if (userSubscription && userSubscription.stripeCustomerId) {
const stripeSession = await stripe.billingPortal.sessions.create({
customer: userSubscription.stripeCustomerId,
return_url: settingsUrl,
});

return new NextResponse(JSON.stringify({ url: stripeSession.url }));
}

const stripeSession = await stripe.checkout.sessions.create({
success_url: settingsUrl,
cancel_url: settingsUrl,
payment_method_types: ['card'],
mode: 'subscription',
billing_address_collection: 'auto',
customer_email: user.emailAddresses[0].emailAddress,
line_items: [
// Only one tier option for now - Pro
{
price_data: {
currency: 'USD',
product_data: {
name: 'Chat With Pro',
description: 'Create Your Own Custom AI Companions',
},
unit_amount: 999,
recurring: {
interval: 'month',
},
},
quantity: 1,
},
],
// Maps the user subscription to the current login user in our Database
metadata: {
userId,
},
});

console.log('Stripe session', stripeSession);

return new NextResponse(JSON.stringify({ url: stripeSession.url }));
} catch (error) {
console.log('[STRIPE_GET_ERROR]', error);
return new NextResponse('Internal Error', { status: 500 });
}
}
67 changes: 67 additions & 0 deletions app/api/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Stripe from 'stripe';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';

import prismadb from '@/lib/prismadb';
import { stripe } from '@/lib/stripe';

export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get('Stripe-Signature') as string;

let event: Stripe.Event;

try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (error: any) {
return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
}

const session = event.data.object as Stripe.Checkout.Session;

if (event.type === 'checkout.session.completed') {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
);

if (!session?.metadata?.userId) {
return new NextResponse('User id is required', { status: 400 });
}

await prismadb.userSubscription.create({
data: {
userId: session?.metadata?.userId,
stripeSubscriptionId: subscription.id,
stripeCustomerId: subscription.customer as string,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
},
});
}

if (event.type === 'invoice.payment_succeeded') {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
);

await prismadb.userSubscription.update({
where: {
stripeSubscriptionId: subscription.id,
},
data: {
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
},
});
}

return new NextResponse(null, { status: 200 });
}
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ClerkProvider } from '@clerk/nextjs';
import { ThemeProvider } from '@/components/theme-provider';
import { cn } from '@/lib/utils';
import { Toaster } from '@/components/ui/toaster';
import { ProModal } from '@/components/pro-modal';

const inter = Inter({ subsets: ['latin'] });

Expand All @@ -24,6 +25,7 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning>
<body className={cn('bg-secondary', inter.className)}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ProModal />
{children}
<Toaster />
</ThemeProvider>
Expand Down
1 change: 0 additions & 1 deletion components/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const ChatMessages = ({
useEffect(() => {
scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages.length]);
console.log('Message: ', messages[messages.length - 1]);

return (
<div className="flex-1 overflow-y-auto pr-4">
Expand Down
2 changes: 1 addition & 1 deletion components/companions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const Companions = ({ data }: CompanionsProps) => {
<div className="pt-10 flex flex-col items-center justify-center space-y-3">
<div className="relative w-60 h-60">
<Image
src="/Empty1.png"
src="/empty.png"
alt="Empty"
fill
className="grayscale rounded-full"
Expand Down
24 changes: 19 additions & 5 deletions components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@ import { UserButton } from '@clerk/nextjs';
import { Button } from './ui/button';
import { ModeToggle } from './mode-toggle';
import MobileSidebar from './mobile-sidebar';
import { useProModal } from '@/hooks/use-pro-modal';

const font = Poppins({ weight: '600', subsets: ['latin'] });

const Navbar = () => {
interface NavbarProps {
isPro: boolean;
}

const Navbar = ({ isPro }: NavbarProps) => {
const proModal = useProModal();

return (
<div className="fixed w-full z-50 flex justify-between items-center py-2 px-4 border-b border-primary/10 bg-secondary h-16">
<div className="flex items-center">
Expand All @@ -29,10 +36,17 @@ const Navbar = () => {
</Link>
</div>
<div className="flex items-center gap-x3">
<Button variant="premium" size="sm" className="mr-4">
Upgrade
<Sparkles className="h-4 w-4 fill-white text-white my-2" />
</Button>
{!isPro && (
<Button
onClick={proModal.onOpen}
variant="premium"
size="sm"
className="mr-4"
>
Upgrade
<Sparkles className="h-4 w-4 fill-white text-white ml-2" />
</Button>
)}
<ModeToggle />
<UserButton afterSignOutUrl="/" />
</div>
Expand Down
Loading

0 comments on commit cb7914f

Please sign in to comment.