Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 67 additions & 100 deletions apps/masterbots.ai/app/api/payment/subscription/route.tsx
Original file line number Diff line number Diff line change
@@ -1,160 +1,127 @@
import type { NextRequest } from 'next/server'
import { Stripe } from 'stripe'
import { NextRequest, NextResponse } from 'next/server';
import { Stripe } from 'stripe';

export const runtime = "edge"

const stripeSecretKey = process.env.STRIPE_SECRET_KEY;

if (!stripeSecretKey) {
if (!stripeSecretKey) {
throw new Error('Stripe secret key is not set.');
}
const stripe = new Stripe(stripeSecretKey|| '', {

const stripe = new Stripe(stripeSecretKey, {
apiVersion: '2024-04-10'
})
});

// # Get Subscription Details by Payment Intent ID
// Get Subscription Details by Payment Intent ID
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const paymentIntentId = searchParams.get('paymentIntentId')
const { searchParams } = new URL(req.url);
const paymentIntentId = searchParams.get('paymentIntentId');

if (!paymentIntentId) {
return new Response(
JSON.stringify({ error: 'paymentIntentId is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
)
return new NextResponse(JSON.stringify({ error: 'paymentIntentId is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}

const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId,
// expand card details
{
expand: ['payment_method']
}
);
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId, {
expand: ['payment_method'],
});

if (!paymentIntent) {
return new Response(
JSON.stringify({ error: 'Payment Intent not found' }),
{
status: 404,
headers: { 'Content-Type': 'application/json' }
}
)
return new NextResponse(JSON.stringify({ error: 'Payment Intent not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}

const invoice = await stripe.invoices.retrieve(
paymentIntent.invoice as string
)
const invoice = await stripe.invoices.retrieve(paymentIntent.invoice as string);

if (!invoice) {
return new Response(JSON.stringify({ error: 'Invoice not found' }), {
return new NextResponse(JSON.stringify({ error: 'Invoice not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
})
headers: { 'Content-Type': 'application/json' },
});
}

const subscriptionId = invoice.subscription
const subscriptionId = invoice.subscription;

if (!subscriptionId) {
return new Response(
JSON.stringify({ error: 'Subscription ID not found in invoice' }),
{
status: 404,
headers: { 'Content-Type': 'application/json' }
}
)
return new NextResponse(JSON.stringify({ error: 'Subscription ID not found in invoice' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}

const subscription = await stripe.subscriptions.retrieve(
subscriptionId as string,
{
expand: ['items.data.plan', 'customer'] // Expand the plan details
}
)

const subscription = await stripe.subscriptions.retrieve(subscriptionId as string, {
expand: ['items.data.plan', 'customer'],
});

const card = paymentIntent.payment_method;


return new Response(JSON.stringify(
{
card,
subscription,
}
), {
return new NextResponse(JSON.stringify({ card, subscription }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
console.error('Error creating subscription:', error)
const stripeError = error?.raw || error
return new Response(JSON.stringify({ error: stripeError?.message }), {
console.error('Error creating subscription:', error);
const stripeError = error?.raw || error;
return new NextResponse(JSON.stringify({ error: stripeError?.message }), {
status: stripeError?.statusCode || 500,
headers: { 'Content-Type': 'application/json' }
})
headers: { 'Content-Type': 'application/json' },
});
}
}

// Use PUT to check if a customer has an active subscription or not by email address
// Use PUT to check if a customer has an active subscription or not by email address
export async function PUT(req: NextRequest) {
try {
const { email } = await req.json()
const { email } = await req.json();
if (!email) {
return new Response(
JSON.stringify({ error: 'Email is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
)
return new NextResponse(JSON.stringify({ error: 'Email is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}

// Search for an existing customer by email
const customers = await stripe.customers.list({
email,
limit: 1
})
const customers = await stripe.customers.list({ email, limit: 1 });

let customer
let customer;
if (customers.data.length > 0) {
// Use the existing customer
customer = customers.data[0]
customer = customers.data[0];
} else {
return new Response(
JSON.stringify({ error: 'Customer not found' }),
{
status: 404,
headers: { 'Content-Type': 'application/json' }
}
)
return new NextResponse(JSON.stringify({ error: 'Customer not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}

const subscriptions = await stripe.subscriptions.list({
customer: customer.id,
status: 'active',
limit: 1
})
limit: 1,
});

if (subscriptions.data.length > 0) {
return new Response(JSON.stringify({ active: true }), {
return new NextResponse(JSON.stringify({ active: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
headers: { 'Content-Type': 'application/json' },
});
} else {
return new Response(JSON.stringify({ active: false }), {
return new NextResponse(JSON.stringify({ active: false }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
headers: { 'Content-Type': 'application/json' },
});
}
} catch (error: any) {
console.error('Error checking subscription:', error)
const stripeError = error?.raw || error
return new Response(JSON.stringify({ error: stripeError?.message }), {
console.error('Error checking subscription:', error);
const stripeError = error?.raw || error;
return new NextResponse(JSON.stringify({ error: stripeError?.message }), {
status: stripeError?.statusCode || 500,
headers: { 'Content-Type': 'application/json' }
})
headers: { 'Content-Type': 'application/json' },
});
}
}

4 changes: 2 additions & 2 deletions apps/masterbots.ai/components/checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ export function InnerCheckout({ prev, next }: WizardStepProps) {
*charged once every {getCurrentOrTargetDate()}
</span>
</div>
<span>${4.49 * 12}</span>
<span>${ plan?.product?.name?.toLowerCase().includes('year') ? (4.49 * 12) : price }</span>
</div>
{plan?.product.name.toLowerCase().includes('year') && (
{plan?.product?.name?.toLowerCase().includes('year') && (
<div className="flex justify-between text-gray-400 mt-3">
<span>
{' '}
Expand Down
4 changes: 2 additions & 2 deletions apps/masterbots.ai/components/plans.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function Plans({ next, goTo }: PlansPros) {
<div className="flex flex-col size-full space-y-3 p-5">
<div
className={cn(
'border-gradient w-full h-[135px] dark:[&>_div]:hover:bg-tertiary',
'border-gradient w-full md:h-[135px] z-0 dark:[&>_div]:hover:bg-tertiary',
{
'selected': selectedPlan === 'free'
}
Expand Down Expand Up @@ -155,7 +155,7 @@ export function Plans({ next, goTo }: PlansPros) {
</div>
</label>
</div>
<div className="flex space-x-3">
<div className="flex md:space-x-3 md:flex-row flex-col space-y-3 md:space-y-0">
{plans && plans.length && (
plans?.filter(plan => plan.active).sort((a, b) => a.created - b.created).map(plan => (
<PlanCard
Expand Down
6 changes: 4 additions & 2 deletions apps/masterbots.ai/components/receipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,17 @@ export const Receipt: React.FC<ReceiptProps> = ({ intentid }) => {
<strong> {getDate(subscription.current_period_start)}</strong>
</span>
</div>
<span>${price}</span>
<span>$ { plan.interval === 'year' ? (4.49 * 12) : price }</span>
</div>
{plan.interval === 'year' && (
<div className="flex justify-between text-gray-400 mt-3">
<span>
{' '}
<strong>Year Plan</strong> subscription discount
</span>
<span>-$0.00</span>
<span>-${((4.49 * 12) - Number(price)).toFixed(2)}</span>
</div>
)}
<div className="flex justify-between mt-5 pb-4 border-b">
<span className="font-bold"> Subtotal</span>
<span>${price}</span>
Expand Down
2 changes: 2 additions & 0 deletions apps/masterbots.ai/components/subscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ export default function Subscription({ user }: { user: { email: string; name: st
handleSetUser(user)

const handleCloseWizard = async () => {

const del = await handleDeleteCustomer(user?.email)
handleSetLoading(false)
handleSetError('')
if (!openDialog) return router.push('/c/p')
if (del) return router.push('/chat')
}

Expand Down
46 changes: 25 additions & 21 deletions apps/masterbots.ai/components/ui/wizard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LoadingState } from '@/components/loading-state'
import { usePayment } from '@/lib/hooks/use-payment'
import { motion } from 'framer-motion'
import { motion } from 'framer-motion'
import React from 'react'
import { useWizard } from './hook/useWizard'
import {
Expand Down Expand Up @@ -46,40 +46,44 @@ const DialogWizard: React.FC<DialogWizardProps> = ({
handleCloseWizard,
errorComponent
}) => {

return (
<Dialog open={dialogOpen} onOpenChange={handleCloseWizard}>
<DialogContent
className="rounded-sm min-h-[540px] w-11/12 p-0 max-w-2xl z-50 bg-gray-100 dark:bg-[#27272A] border border-black"
>
<DialogHeader className="flex justify-between mb-0 items-center dark:bg-[#1E293B] bg-gray-200 dark:text-white text-black p-5 pb-10">
<DialogTitle>{headerTitle}</DialogTitle>
<DialogContent className="rounded-sm max-h-screen md:min-h-[540px] w-full md:w-11/12 p-0 md:max-w-2xl z-50 bg-gray-100 dark:bg-[#27272A] border border-black overflow-y-scroll">
<DialogHeader className="sticky top-0 flex z-50 md:max-h-auto max-h-20 justify-between mb-0 items-center dark:bg-[#1E293B] bg-gray-200 dark:text-white text-black p-5 pb-10">
<DialogTitle>{headerTitle}</DialogTitle>
</DialogHeader>
<Content
errorComponent={errorComponent}
steps={steps}
dialogOpen={dialogOpen}

/>
<div className="">
<Content
errorComponent={errorComponent}
steps={steps}
dialogOpen={dialogOpen}
/>
</div>
</DialogContent>
</Dialog>
)
}

function Content({ errorComponent, steps, dialogOpen }: { errorComponent?: JSX.Element, steps: WizardStep[], dialogOpen: boolean }) {
function Content({
errorComponent,
steps,
dialogOpen
}: {
errorComponent?: JSX.Element
steps: WizardStep[]
dialogOpen: boolean
}) {
const { error, loading } = usePayment()
const { close, Next, Prev, goTo, lastStep, currentStep } = useWizard(steps, dialogOpen)
const defaultErrorComponent = () => (
<div>{error}</div>
const { close, Next, Prev, goTo, lastStep, currentStep } = useWizard(
steps,
dialogOpen
)
const defaultErrorComponent = () => <div>{error}</div>
const ErrorComponent = (() => errorComponent) || defaultErrorComponent

if (error && error !== '') {
return (
<motion.div
key="wizard-error-container"
{...animationStepProps}
>
<motion.div key="wizard-error-container" {...animationStepProps}>
<ErrorComponent />
</motion.div>
)
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"packageManager": "bun@1.0.14",
"dependencies": {
"axios": "^1.7.2",
"nextjs-toploader": "^1.6.4"

"nanoid": "latest",
"next": "latest",
"nextjs-toploader": "^1.6.4",
"postcss": "latest"
}
}