Skip to content

Commit

Permalink
Stripe changes to only one time payment(#98)
Browse files Browse the repository at this point in the history
* added userProducts database table

* teamsData replaced by userProducts

* submit button disabled if product is bought

* checkout success route adds userProduct

* Update to use VERCEL_URL in stripe

* Change base url logic

* Updates in FAQ and pricing page

---------

Co-authored-by: Rodrigo Elizeu Cherutti <[email protected]>
  • Loading branch information
BrunoSette and PregoBS authored Oct 30, 2024
1 parent 2af2b3b commit 57e3fb0
Show file tree
Hide file tree
Showing 18 changed files with 1,907 additions and 64 deletions.
20 changes: 13 additions & 7 deletions app/(dashboard)/dashboard/newtest/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
import { Settings } from "./settings";
import { getTeamForUser, getUser } from "@/lib/db/queries";
import { getProductsForUser, getUser } from "@/lib/db/queries";
import { UserProduct } from "@/lib/db/schema";

export default async function NewTestPage() {
const user = await getUser();
Expand All @@ -10,21 +11,26 @@ export default async function NewTestPage() {
redirect("/sign-in");
}

console.log("User found:", user);
// console.log("User found:", user);

let teamData;
// let teamData;
let userProducts: UserProduct[] = [];
try {
teamData = await getTeamForUser(user.id);
console.log("Team data fetched successfully:", teamData);
// teamData = await getTeamForUser(user.id);
userProducts = await getProductsForUser(user.id);
// console.log("User Products data fetched successfully:", userProducts);
// console.log("Team data fetched successfully:", teamData);
} catch (error) {
console.error("Error fetching team data:", error);
// Instead of redirecting, we'll pass null to the Settings component
teamData = null;
// teamData = null;
}

return (
<div>
<Settings teamData={teamData} />
<Settings userProducts={userProducts} />
</div>
);
}


28 changes: 14 additions & 14 deletions app/(dashboard)/dashboard/newtest/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Loader2 } from "lucide-react";
import { TeamDataWithMembers } from "@/lib/db/schema";
import { Products, subjects } from "@/lib/utils";
import { UserProduct } from "@/lib/db/schema";
import { mergeProductNames, Products, subjects } from "@/lib/utils";
import { PricingDialog } from "@/components/ManageSubscriptionDialog";
import Head from "next/head";

Expand All @@ -18,9 +18,11 @@ const metadata = {
};

export function Settings({
teamData,
userProducts,
// teamData,
}: {
teamData: TeamDataWithMembers | null;
userProducts: UserProduct[];
// teamData: TeamDataWithMembers | null;
}) {
const router = useRouter();
const [isTutor, setIsTutor] = useState(true);
Expand All @@ -34,11 +36,10 @@ export function Settings({
const [error, setError] = useState("");
const [showPricingDialog, setShowPricingDialog] = useState(false);

const subscriptionStatus = teamData?.subscriptionStatus || "free";
const planName = teamData?.planName || "none";
const subscriptionStatus = userProducts.some((p) => p.active) ? "active" : "free";
const planName = mergeProductNames(userProducts);

const isActiveOrTrialing =
subscriptionStatus === "active" || subscriptionStatus === "trialing";
const isActiveOrTrialing = subscriptionStatus === "active";

useEffect(() => {
if (!isActiveOrTrialing) {
Expand Down Expand Up @@ -121,9 +122,8 @@ export function Settings({
onChange={() => handleSubjectChange(subject.id, isBarrister)}
disabled={
!(
(planName === Products[productIndex].name &&
isActiveOrTrialing) ||
(planName === Products[2].name && isActiveOrTrialing)
(userProducts.some((p) => p.stripeProductName === Products[productIndex].name) && isActiveOrTrialing) ||
(userProducts.some((p) => p.stripeProductName === Products[2].name) && isActiveOrTrialing)
)
}
/>
Expand All @@ -136,8 +136,8 @@ export function Settings({
</Label>
</div>
))}
{((planName === Products[productIndex].name && isActiveOrTrialing) ||
(planName === Products[2].name && isActiveOrTrialing)) && (
{((userProducts.some((p) => p.stripeProductName === Products[productIndex].name) && isActiveOrTrialing) ||
(userProducts.some((p) => p.stripeProductName === Products[2].name) && isActiveOrTrialing)) && (
<Button
type="button"
className="bg-orange-500 mt-4 hover:bg-orange-600 text-white"
Expand All @@ -159,7 +159,7 @@ export function Settings({
selectedSolicitorSubjects,
handleSubjectChange,
handleSelectAllSubjects,
planName,
userProducts,
isActiveOrTrialing,
]
);
Expand Down
13 changes: 7 additions & 6 deletions app/(dashboard)/dashboard/subscription/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { redirect } from 'next/navigation';
import { Settings } from './settings';
import { getTeamForUser, getUser } from '@/lib/db/queries';
import { getProductsForUser, getUser } from '@/lib/db/queries';

export default async function SubscriptionPage() {
const user = await getUser();
Expand All @@ -9,11 +9,12 @@ export default async function SubscriptionPage() {
redirect('/login');
}

const teamData = await getTeamForUser(user.id);
// const teamData = await getTeamForUser(user.id);
const userProducts = await getProductsForUser(user.id);

if (!teamData) {
throw new Error('Team not found');
}
// if (!userProducts.length) {
// throw new Error('Products not found');
// }

return <Settings teamData={teamData} />;
return <Settings userProducts={userProducts} />;
}
20 changes: 15 additions & 5 deletions app/(dashboard)/dashboard/subscription/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { customerPortalAction } from "@/lib/payments/actions";
import { TeamDataWithMembers } from "@/lib/db/schema";
import { UserProduct } from "@/lib/db/schema";
import { mergeProductNames } from "@/lib/utils";

export function Settings({
userProducts,
// teamData
}: {
userProducts: UserProduct[];
// teamData: TeamDataWithMembers;
}) {
const planName = mergeProductNames(userProducts);


export function Settings({ teamData }: { teamData: TeamDataWithMembers }) {
return (
<section className="flex-1 p-4 lg:p-8">
<h1 className="text-lg lg:text-2xl font-medium mb-6">Subscription</h1>
Expand All @@ -14,15 +24,15 @@ export function Settings({ teamData }: { teamData: TeamDataWithMembers }) {
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center">
<div className="mb-4 sm:mb-0">
<p className="font-medium">
Current Plan: {teamData.planName || "Free"}
Current Plan: {planName}
</p>
<p className="text-sm text-muted-foreground">
{/* <p className="text-sm text-muted-foreground">
{teamData.subscriptionStatus === "active"
? "Billed monthly"
: teamData.subscriptionStatus === "trialing"
? "Trial period"
: "No active subscription"}
</p>
</p> */}
</div>
<form action={customerPortalAction}>
<Button type="submit" variant="outline">
Expand Down
20 changes: 12 additions & 8 deletions app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ export default function HomePage() {
</h2>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2 bg-orange-50">
{[
{
question: "How up-to-date is your question bank?",
answer:
"Our question bank is regularly updated to reflect the latest changes in Ontario Bar Exam content and format.",
},
{
question: "How up-to-date is your question bank?",
answer:
Expand All @@ -228,21 +233,20 @@ export default function HomePage() {
"Our system tracks your performance across different topics and question types, providing detailed insights to help you improve.",
},
{
question:
"What is included in the 30-day money back guarantee?",
question: "What payment methods do you accept?",
answer:
"With our 30-day money back guarantee, you can try BarQuest risk-free. If you're not satisfied within the first 30 days, simply send an email to [email protected] and we'll refund your order immediately, no questions asked.",
"We accept all major credit cards, including Visa, MasterCard, American Express, and Discover. Payments are processed securely through Stripe.",
},
{
question: "How do I cancel if I'm not satisfied?",
question:
"Can I take unlimited quizzes with my BarQuest purchase?",
answer:
"Canceling is easy! If you decide that BarQuest isn't the right fit for you in the first 30 days, simply send an email to [email protected] and we'll refund your order immediately, no questions asked.",
"Absolutely! With your one-time BarQuest purchase, you have access to create and take as many quizzes as you want for a full 90 days. This unlimited access lets you thoroughly prepare and revisit questions to ensure you're exam-ready.",
},

{
question: "What payment methods do you accept?",
question: "How many questions are included with BarQuest?",
answer:
"We accept all major credit cards, including Visa, MasterCard, American Express, and Discover. Payments are processed securely through Stripe.",
"BarQuest offers three comprehensive packages: Barrister (+500 questions), Solicitor (+600 questions), and Full (+1,000 questions). Each package includes detailed commentary and covers all relevant exam topics.",
},
].map((item, index) => (
<div key={index} className="bg-white p-6 rounded-lg">
Expand Down
14 changes: 13 additions & 1 deletion app/(dashboard)/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ import { Check } from "lucide-react";
import { getStripePrices, getStripeProducts } from "@/lib/payments/stripe";
import { SubmitButton } from "./submit-button";
import { Products } from "@/lib/utils";
import { getProductsForUser, getUser } from "@/lib/db/queries";
import { UserProduct } from "@/lib/db/schema";

// Prices are fresh for one hour max
export const revalidate = 3600;

export default async function PricingPage() {
const user = await getUser();
let userProducts: UserProduct[] = [];

if (user) {
userProducts = await getProductsForUser(user.id);
}

const [prices, products] = await Promise.all([
getStripePrices(),
getStripeProducts(),
Expand Down Expand Up @@ -55,6 +64,7 @@ export default async function PricingPage() {
trialDays={trialDays}
features={features}
priceId={stripePrice.id}
isPurchased={userProducts.some((p) => p.active && p.stripeProductName == name)}
/>
)
)}
Expand All @@ -69,12 +79,14 @@ export default async function PricingPage() {
trialDays,
features,
priceId,
isPurchased,
}: {
name: string;
price: number | null;
interval: string;
trialDays: number;
features: string[];
isPurchased: boolean;
priceId?: string;
}) {
return (
Expand All @@ -101,7 +113,7 @@ export default async function PricingPage() {
</ul>
<form action={checkoutAction} className="w-full flex justify-center">
<input type="hidden" name="priceId" value={priceId} />
<SubmitButton />
<SubmitButton disabled={isPurchased} />
</form>
</div>
);
Expand Down
14 changes: 9 additions & 5 deletions app/(dashboard)/pricing/submit-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { Button } from "@/components/ui/button";
import { ArrowRight, Loader2 } from "lucide-react";
import { useFormStatus } from "react-dom";

export function SubmitButton() {
export function SubmitButton({ disabled = false }: { disabled: boolean }) {
const { pending } = useFormStatus();

const btnStyle = disabled
? "bg-orange-600 text-white"
: "bg-white hover:bg-orange-400 hover:text-white text-black";

return (
<Button
type="submit"
disabled={pending}
className="w-full bg-white hover:bg-orange-400 hover:text-white text-black border border-gray-200 rounded-full flex text-lg items-center justify-center"
disabled={pending || disabled}
className={`w-full ${btnStyle} border border-gray-200 rounded-full flex text-lg items-center justify-center`}
>
{pending ? (
<div className="flex items-center">
Expand All @@ -20,8 +24,8 @@ export function SubmitButton() {
</div>
) : (
<div className="flex items-center">
Get Started
<ArrowRight className="ml-2 h-4 w-4" />
{disabled ? "Purchased!" : "Get Started"}
{!disabled && <ArrowRight className="ml-2 h-4 w-4" />}
</div>
)}
</Button>
Expand Down
2 changes: 1 addition & 1 deletion app/(login)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const MODES = {
signin: {
title: "Sign in to your account",
buttonText: "Sign in",
altLink: { text: "Start My 7 Days Free Trial", href: "/#pricing" },
altLink: { text: "Create my Account", href: "/#pricing" },
altPrompt: "New to our platform?",
},
signup: {
Expand Down
34 changes: 25 additions & 9 deletions app/api/stripe/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { eq } from "drizzle-orm";
import { db } from "@/lib/db/drizzle";
import { users, teams, teamMembers } from "@/lib/db/schema";
import { users, teams, teamMembers, userProducts } from "@/lib/db/schema";
import { setSession } from "@/lib/auth/session";
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/payments/stripe";
Expand All @@ -23,16 +23,21 @@ export async function GET(request: NextRequest) {
if (!session.customer || typeof session.customer === "string") {
console.warn("Customer data is not available or is a string.");
// Handle logic for sessions without a customer, if applicable
return NextResponse.redirect(new URL("/dashboard", request.url));
// return NextResponse.redirect(new URL("/dashboard", request.url));
}

const lineItems = session.line_items?.data;
if (!lineItems || lineItems.length === 0) {
throw new Error('No line items found for this session.');
}

console.log("lineitems:", JSON.stringify(lineItems, null, 4));
const product = lineItems[0].price?.product;
const price = lineItems[0].price;
if (!price) {
throw new Error('No product price found for this session.');
}

// console.log("lineitems:", JSON.stringify(lineItems, null, 4));
const product = price.product;
if (!product) {
throw new Error('No product found for this session.');
}
Expand All @@ -42,9 +47,11 @@ export async function GET(request: NextRequest) {
? product
: product?.id;

console.log("productId:", productId);
const productName: string = (product as Stripe.Product).name;

// console.log("product:", JSON.stringify(product, null, 4));

const customerId = session.customer.id;
// const customerId = session.customer.id;
// const subscriptionId =
// typeof session.subscription === 'string'
// ? session.subscription
Expand Down Expand Up @@ -106,15 +113,24 @@ export async function GET(request: NextRequest) {
await db
.update(teams)
.set({
stripeCustomerId: customerId,
// stripeCustomerId: customerId,
// stripeSubscriptionId: null, //subscriptionId,
stripeProductId: productId,
// planName: null, //(plan.product as Stripe.Product).name,
// subscriptionStatus: null, //subscription.status,
planName: productName,
subscriptionStatus: "active",
updatedAt: new Date(),
})
.where(eq(teams.id, userTeam[0].teamId));

await db
.insert(userProducts)
.values({
userId: Number(userId),
stripeProductId: productId,
stripeProductName: productName,
stripePriceId: price.id,
});

await setSession(user[0]);
return NextResponse.redirect(new URL("/dashboard", request.url));
} catch (error) {
Expand Down
Loading

0 comments on commit 57e3fb0

Please sign in to comment.