stripe frontend

This commit is contained in:
2026-01-28 18:33:30 +00:00
parent 6cf7e79f20
commit 98ff4014cc
10 changed files with 504 additions and 109 deletions

View File

@@ -29,8 +29,14 @@ export async function updateById(
passwordHash?: string; passwordHash?: string;
avatarURL?: string | null; avatarURL?: string | null;
iconPreference?: IconStyle; iconPreference?: IconStyle;
plan?: string;
}, },
): Promise<UserRecord | undefined> { ): Promise<UserRecord | undefined> {
const [user] = await db.update(User).set(updates).where(eq(User.id, id)).returning(); const [user] = await db.update(User).set(updates).where(eq(User.id, id)).returning();
return user; return user;
} }
export async function updateUser(id: number, updates: { plan?: string }) {
const [user] = await db.update(User).set(updates).where(eq(User.id, id)).returning();
return user;
}

View File

@@ -1,6 +1,7 @@
import type { IconStyle } from "@sprint/shared"; import type { IconStyle } from "@sprint/shared";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { useAuthenticatedSession } from "@/components/session-provider"; import { useAuthenticatedSession } from "@/components/session-provider";
import ThemeToggle from "@/components/theme-toggle"; import ThemeToggle from "@/components/theme-toggle";
@@ -161,7 +162,20 @@ function Account({ trigger }: { trigger?: ReactNode }) {
{error !== "" && <Label className="text-destructive text-sm">{error}</Label>} {error !== "" && <Label className="text-destructive text-sm">{error}</Label>}
<div className="flex justify-end mt-4"> {/* Show subscription management link */}
<div className="pt-2">
{currentUser.plan === "pro" ? (
<Button asChild className="w-fit bg-personality hover:bg-personality/90 font-700">
<Link to="/plans">Manage subscription</Link>
</Button>
) : (
<Button asChild className="w-fit bg-personality hover:bg-personality/90 font-700">
<Link to="/plans">Upgrade to Pro</Link>
</Button>
)}
</div>
<div className="flex justify-end mt-2">
<Button variant={"outline"} type={"submit"} className="px-12"> <Button variant={"outline"} type={"submit"} className="px-12">
Save Save
</Button> </Button>

View File

@@ -0,0 +1,120 @@
import { Button } from "@/components/ui/button";
import Icon from "@/components/ui/icon";
import { cn } from "@/lib/utils";
export interface PricingTier {
name: string;
price: string;
priceAnnual: string;
period: string;
periodAnnual: string;
description: string;
tagline: string;
features: string[];
cta: string;
highlighted: boolean;
}
export function PricingCard({
tier,
billingPeriod,
onCtaClick,
disabled = false,
loading = false,
}: {
tier: PricingTier;
billingPeriod: "monthly" | "annual";
onCtaClick: () => void;
disabled?: boolean;
loading?: boolean;
}) {
return (
<div
className={cn(
"flex flex-col border p-8 space-y-6 relative",
tier.highlighted ? "border-2 border-personality shadow-lg scale-105" : "border-border",
)}
>
{tier.highlighted && (
<div className="absolute -top-4 left-4 bg-personality text-background px-3 py-1 text-xs font-700">
{tier.tagline}
</div>
)}
<div className="space-y-4">
<h3 className="text-3xl font-basteleur font-700">{tier.name}</h3>
<div className="flex items-baseline gap-2">
<span className="text-4xl font-700">
{billingPeriod === "annual" ? tier.priceAnnual : tier.price}
</span>
<span className="text-sm text-muted-foreground">
{billingPeriod === "annual" ? tier.periodAnnual : tier.period}
</span>
</div>
<p className="text-muted-foreground">{tier.description}</p>
</div>
<ul className="space-y-3 flex-1">
{tier.features.map((feature) => (
<li key={feature} className="flex items-start gap-2 text-sm">
<Icon icon="check" iconStyle={"pixel"} className="size-6 -mt-0.5" color="var(--personality)" />
<span>{feature}</span>
</li>
))}
</ul>
<Button
variant={tier.highlighted ? "default" : "outline"}
className={cn(
"font-700 py-6",
tier.highlighted ? "bg-personality hover:bg-personality/90 text-background" : "",
)}
onClick={onCtaClick}
disabled={disabled}
>
{loading ? "Processing..." : tier.cta}
</Button>
</div>
);
}
export const pricingTiers: PricingTier[] = [
{
name: "Starter",
price: "£0",
priceAnnual: "£0",
period: "Free forever",
periodAnnual: "Free forever",
description: "Perfect for side projects and solo developers",
tagline: "For solo devs and small projects",
features: [
"1 organisation (owned or joined)",
"1 project",
"100 issues",
"Up to 5 team members",
"Email support",
],
cta: "Get started free",
highlighted: false,
},
{
name: "Pro",
price: "£11.99",
priceAnnual: "£9.99",
period: "per user/month",
periodAnnual: "per user/month",
description: "For growing teams and professionals",
tagline: "Most Popular",
features: [
"Everything in starter",
"Unlimited organisations",
"Unlimited projects",
"Unlimited issues",
"Advanced time tracking & reports",
"Custom issue statuses",
"Priority email support",
],
cta: "Upgrade to Pro",
highlighted: true,
},
];

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import Account from "@/components/account"; import Account from "@/components/account";
import { IssueForm } from "@/components/issue-form"; import { IssueForm } from "@/components/issue-form";
import LogOutButton from "@/components/log-out-button"; import LogOutButton from "@/components/log-out-button";
@@ -11,6 +11,7 @@ import { useSelection } from "@/components/selection-provider";
import { useAuthenticatedSession } from "@/components/session-provider"; import { useAuthenticatedSession } from "@/components/session-provider";
import SmallUserDisplay from "@/components/small-user-display"; import SmallUserDisplay from "@/components/small-user-display";
import { SprintForm } from "@/components/sprint-form"; import { SprintForm } from "@/components/sprint-form";
import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -122,6 +123,11 @@ export default function TopBar({ showIssueForm = true }: { showIssueForm?: boole
)} )}
</div> </div>
<div className={`flex gap-${BREATHING_ROOM} items-center`}> <div className={`flex gap-${BREATHING_ROOM} items-center`}>
{user.plan !== "pro" && (
<Button asChild className="bg-personality hover:bg-personality/90 text-background font-600">
<Link to="/plans">Upgrade</Link>
</Button>
)}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="text-sm"> <DropdownMenuTrigger className="text-sm">
<SmallUserDisplay user={user} /> <SmallUserDisplay user={user} />

View File

@@ -0,0 +1,38 @@
import { getServerURL } from "@/lib/utils";
interface CreateCheckoutParams {
billingPeriod: "monthly" | "annual";
csrfToken: string;
onSuccess?: (url: string) => void;
onError?: (error: string) => void;
}
export async function createCheckoutSession({
billingPeriod,
csrfToken,
onSuccess,
onError,
}: CreateCheckoutParams) {
try {
const response = await fetch(`${getServerURL()}/subscription/create-checkout-session`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
credentials: "include",
body: JSON.stringify({ billingPeriod }),
});
const data = await response.json();
if (!response.ok) {
onError?.(data.error || "Failed to create checkout session");
return;
}
onSuccess?.(data.url);
} catch (error) {
onError?.("Network error");
}
}

View File

@@ -0,0 +1,31 @@
import { getServerURL } from "@/lib/utils";
interface CreatePortalParams {
csrfToken: string;
onSuccess?: (url: string) => void;
onError?: (error: string) => void;
}
export async function createPortalSession({ csrfToken, onSuccess, onError }: CreatePortalParams) {
try {
const response = await fetch(`${getServerURL()}/subscription/create-portal-session`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
credentials: "include",
});
const data = await response.json();
if (!response.ok) {
onError?.(data.error || "Failed to create portal session");
return;
}
onSuccess?.(data.url);
} catch (error) {
onError?.("Network error");
}
}

View File

@@ -0,0 +1,27 @@
import type { SubscriptionRecord } from "@sprint/shared";
import { getServerURL } from "@/lib/utils";
interface GetSubscriptionParams {
onSuccess?: (subscription: SubscriptionRecord | null) => void;
onError?: (error: string) => void;
}
export async function getSubscription({ onSuccess, onError }: GetSubscriptionParams) {
try {
const response = await fetch(`${getServerURL()}/subscription/get`, {
method: "GET",
credentials: "include",
});
const data = await response.json();
if (!response.ok) {
onError?.(data.error || "Failed to fetch subscription");
return;
}
onSuccess?.(data.subscription);
} catch (error) {
onError?.("Network error");
}
}

View File

@@ -12,6 +12,8 @@ import Font from "@/pages/Font";
import Issues from "@/pages/Issues"; import Issues from "@/pages/Issues";
import Landing from "@/pages/Landing"; import Landing from "@/pages/Landing";
import NotFound from "@/pages/NotFound"; import NotFound from "@/pages/NotFound";
import Plans from "@/pages/Plans";
import StripeTest from "@/pages/StripeTest";
import Test from "@/pages/Test"; import Test from "@/pages/Test";
import Timeline from "@/pages/Timeline"; import Timeline from "@/pages/Timeline";
@@ -28,6 +30,14 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Route path="/font" element={<Font />} /> <Route path="/font" element={<Font />} />
{/* authed routes */} {/* authed routes */}
<Route
path="/plans"
element={
<RequireAuth>
<Plans />
</RequireAuth>
}
/>
<Route <Route
path="/issues" path="/issues"
element={ element={
@@ -44,6 +54,14 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
</RequireAuth> </RequireAuth>
} }
/> />
<Route
path="/stripe-test"
element={
<RequireAuth>
<StripeTest />
</RequireAuth>
}
/>
<Route <Route
path="/timeline" path="/timeline"
element={ element={

View File

@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { LoginModal } from "@/components/login-modal"; import { LoginModal } from "@/components/login-modal";
import { PricingCard, pricingTiers } from "@/components/pricing-card";
import { useSession } from "@/components/session-provider"; import { useSession } from "@/components/session-provider";
import ThemeToggle from "@/components/theme-toggle"; import ThemeToggle from "@/components/theme-toggle";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -8,57 +9,12 @@ import Icon from "@/components/ui/icon";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const pricingTiers = [
{
name: "Starter",
price: "£0",
priceAnnual: "£0",
period: "Free forever",
periodAnnual: "Free forever",
description: "Perfect for side projects and solo developers",
tagline: "For solo devs and small projects",
features: [
"1 organisation (owned or joined)",
"1 project",
"100 issues",
"Up to 5 team members",
"Email support",
],
cta: "Get started free",
highlighted: false,
},
{
name: "Pro",
price: "£11.99",
priceAnnual: "£9.99",
period: "per user/month",
periodAnnual: "per user/month",
description: "For growing teams and professionals",
tagline: "Most Popular",
features: [
"Everything in starter",
"Unlimited organisations",
"Unlimited projects",
"Unlimited issues",
"Advanced time tracking & reports",
"Custom issue statuses",
"Priority email support",
],
cta: "Try pro free for 14 days",
highlighted: true,
},
];
const faqs = [ const faqs = [
{ {
question: "Can I switch plans?", question: "Can I switch plans?",
answer: answer:
"Yes, you can upgrade or downgrade at any time. Changes take effect immediately, and we'll prorate any charges.", "Yes, you can upgrade or downgrade at any time. Changes take effect immediately, and we'll prorate any charges.",
}, },
{
question: "Is there a free trial?",
answer: "Yes, pro plan includes a 14-day free trial with full access. No credit card required to start.",
},
{ {
question: "What payment methods do you accept?", question: "What payment methods do you accept?",
answer: "We accept all major credit cards.", answer: "We accept all major credit cards.",
@@ -68,11 +24,6 @@ const faqs = [
answer: answer:
"Pro plan pricing scales with your team. Add or remove users anytime, and we'll adjust your billing automatically.", "Pro plan pricing scales with your team. Add or remove users anytime, and we'll adjust your billing automatically.",
}, },
{
question: "What happens when my trial ends?",
answer:
"You'll automatically downgrade to the free starter plan. No charges unless you actively upgrade to pro.",
},
{ {
question: "Can I cancel anytime?", question: "Can I cancel anytime?",
answer: answer:
@@ -86,7 +37,7 @@ const faqs = [
export default function Landing() { export default function Landing() {
const { user, isLoading } = useSession(); const { user, isLoading } = useSession();
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly"); const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("annual");
const [loginModalOpen, setLoginModalOpen] = useState(false); const [loginModalOpen, setLoginModalOpen] = useState(false);
return ( return (
@@ -163,7 +114,7 @@ export default function Landing() {
) : ( ) : (
<> <>
<Button size="lg" className="text-lg px-8 py-6" onClick={() => setLoginModalOpen(true)}> <Button size="lg" className="text-lg px-8 py-6" onClick={() => setLoginModalOpen(true)}>
Start free trial Get started
</Button> </Button>
<Button asChild variant="outline" size="lg" className="text-lg px-8 py-6"> <Button asChild variant="outline" size="lg" className="text-lg px-8 py-6">
<a href="#pricing">See pricing</a> <a href="#pricing">See pricing</a>
@@ -172,7 +123,7 @@ export default function Landing() {
)} )}
</div> </div>
<p className="text-sm text-muted-foreground">No credit card required · Full access for 14 days</p> <p className="text-sm text-muted-foreground">Free forever · Upgrade when you need more</p>
</div> </div>
{/* problem section */} {/* problem section */}
@@ -340,57 +291,12 @@ export default function Landing() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-4xl"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-4xl">
{pricingTiers.map((tier) => ( {pricingTiers.map((tier) => (
<div <PricingCard
key={tier.name} key={tier.name}
className={cn( tier={tier}
"flex flex-col border p-8 space-y-6 relative", billingPeriod={billingPeriod}
tier.highlighted ? "border-2 border-personality shadow-lg scale-105" : "border-border", onCtaClick={() => setLoginModalOpen(true)}
)} />
>
{tier.highlighted && (
<div className="absolute -top-4 left-4 bg-personality text-background px-3 py-1 text-xs font-700">
{tier.tagline}
</div>
)}
<div className="space-y-4">
<h3 className="text-3xl font-basteleur font-700">{tier.name}</h3>
<div className="flex items-baseline gap-2">
<span className="text-4xl font-700">
{billingPeriod === "annual" ? tier.priceAnnual : tier.price}
</span>
<span className="text-sm text-muted-foreground">
{billingPeriod === "annual" ? tier.periodAnnual : tier.period}
</span>
</div>
<p className="text-muted-foreground">{tier.description}</p>
</div>
<ul className="space-y-3 flex-1">
{tier.features.map((feature) => (
<li key={feature} className="flex items-start gap-2 text-sm">
<Icon
icon="check"
iconStyle={"pixel"}
className="size-6 -mt-0.5"
color="var(--personality)"
/>
<span>{feature}</span>
</li>
))}
</ul>
<Button
variant={tier.highlighted ? "default" : "outline"}
className={cn(
"font-700 py-6",
tier.highlighted ? "bg-personality hover:bg-personality/90 text-background" : "",
)}
onClick={() => setLoginModalOpen(true)}
>
{tier.cta}
</Button>
</div>
))} ))}
</div> </div>
@@ -408,8 +314,8 @@ export default function Landing() {
className="size-8" className="size-8"
color="var(--personality)" color="var(--personality)"
/> />
<p className="font-700">No Card Required</p> <p className="font-700">Free Starter Plan</p>
<p className="text-sm text-muted-foreground">Start your trial instantly</p> <p className="text-sm text-muted-foreground">Get started instantly</p>
</div> </div>
<div className="flex flex-col items-center text-center gap-2"> <div className="flex flex-col items-center text-center gap-2">
<Icon icon="rotateCcw" iconStyle={"pixel"} className="size-8" color="var(--personality)" /> <Icon icon="rotateCcw" iconStyle={"pixel"} className="size-8" color="var(--personality)" />
@@ -473,12 +379,12 @@ export default function Landing() {
</Button> </Button>
) : ( ) : (
<Button size="lg" className="text-lg px-8 py-6" onClick={() => setLoginModalOpen(true)}> <Button size="lg" className="text-lg px-8 py-6" onClick={() => setLoginModalOpen(true)}>
Start your free trial Get started
</Button> </Button>
)} )}
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
No credit card required · 14-day free trial · Cancel anytime Free forever · Upgrade when you need more · Cancel anytime
</p> </p>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,229 @@
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { LoginModal } from "@/components/login-modal";
import { PricingCard, pricingTiers } from "@/components/pricing-card";
import { useSession } from "@/components/session-provider";
import { Button } from "@/components/ui/button";
import Icon from "@/components/ui/icon";
import { Switch } from "@/components/ui/switch";
import { createCheckoutSession } from "@/lib/server/subscription/createCheckoutSession";
import { createPortalSession } from "@/lib/server/subscription/createPortalSession";
import { getSubscription } from "@/lib/server/subscription/getSubscription";
import { cn, getCsrfToken } from "@/lib/utils";
interface SubscriptionData {
status: string;
currentPeriodEnd: Date | null;
quantity: number;
}
export default function Plans() {
const { user, isLoading } = useSession();
const navigate = useNavigate();
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("annual");
const [loginModalOpen, setLoginModalOpen] = useState(false);
const [subscription, setSubscription] = useState<SubscriptionData | null>(null);
const [loadingSubscription, setLoadingSubscription] = useState(false);
const [processingTier, setProcessingTier] = useState<string | null>(null);
// fetch subscription if user is logged in
useEffect(() => {
if (user) {
setLoadingSubscription(true);
getSubscription({
onSuccess: (data) => {
setSubscription(data);
setLoadingSubscription(false);
},
onError: () => {
setSubscription(null);
setLoadingSubscription(false);
},
});
}
}, [user]);
const hasProSubscription = subscription?.status === "active";
const csrfToken = getCsrfToken() || "";
const handleTierAction = (tierName: string) => {
if (!user) {
setLoginModalOpen(true);
return;
}
if (tierName === "Pro") {
if (hasProSubscription) {
// open customer portal
setProcessingTier(tierName);
createPortalSession({
csrfToken,
onSuccess: (url) => {
window.location.href = url;
},
onError: () => {
setProcessingTier(null);
},
});
} else {
// start checkout
setProcessingTier(tierName);
createCheckoutSession({
billingPeriod,
csrfToken,
onSuccess: (url) => {
window.location.href = url;
},
onError: () => {
setProcessingTier(null);
},
});
}
}
// starter tier - just go to issues if not already there
if (tierName === "Starter") {
navigate("/issues");
}
};
// modify pricing tiers based on user's current plan
const modifiedTiers = pricingTiers.map((tier) => {
const isCurrentPlan = tier.name === "Pro" && hasProSubscription;
const isStarterCurrent = tier.name === "Starter" && !hasProSubscription;
return {
...tier,
highlighted: isCurrentPlan || (!hasProSubscription && tier.name === "Pro"),
cta: isCurrentPlan
? "Manage subscription"
: isStarterCurrent
? "Current plan"
: tier.name === "Pro"
? "Upgrade to Pro"
: tier.cta,
};
});
return (
<div className="min-h-screen flex flex-col">
<header className="sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-md">
<div className="w-full flex h-14 items-center justify-between px-2">
<div className="flex items-center gap-2">
<img src="/favicon.svg" alt="Sprint" className="size-12 -mt-0.5" />
<span className="text-3xl font-basteleur font-700 transition-colors -mt-0.5">Sprint</span>
</div>
<nav className="flex items-center gap-6">
<Link
to="/"
className="hidden md:block text-sm font-500 hover:text-personality transition-colors"
>
Home
</Link>
<div className="flex items-center gap-2">
{!isLoading && user ? (
<Button asChild variant="outline" size="sm">
<Link to="/issues">Open app</Link>
</Button>
) : (
<Button variant="outline" size="sm" onClick={() => setLoginModalOpen(true)}>
Sign in
</Button>
)}
</div>
</nav>
</div>
</header>
<main className="flex-1 flex flex-col items-center py-16 pt-14 px-4">
<div className="max-w-6xl w-full space-y-16">
<div className="text-center space-y-6">
<h1 className="text-5xl font-basteleur font-700">
{user ? "Choose your plan" : "Simple, transparent pricing"}
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
{user
? hasProSubscription
? "You are currently on the Pro plan. Manage your subscription or switch plans below."
: "You are currently on the Starter plan. Upgrade to Pro for unlimited access."
: "Choose the plan that fits your team. Scale as you grow."}
</p>
{/* billing toggle */}
<div className="flex items-center justify-center gap-4 pt-4">
<button
type="button"
onClick={() => setBillingPeriod("monthly")}
className={cn(
"text-lg transition-colors",
billingPeriod === "monthly" ? "text-foreground font-700" : "text-muted-foreground",
)}
>
monthly
</button>
<Switch
size="lg"
checked={billingPeriod === "annual"}
onCheckedChange={(checked) => setBillingPeriod(checked ? "annual" : "monthly")}
className="bg-border data-[state=checked]:bg-border! data-[state=unchecked]:bg-border!"
thumbClassName="bg-personality dark:bg-personality data-[state=checked]:bg-personality! data-[state=unchecked]:bg-personality!"
aria-label="toggle billing period"
/>
<button
type="button"
onClick={() => setBillingPeriod("annual")}
className={cn(
"text-lg transition-colors",
billingPeriod === "annual" ? "text-foreground font-700" : "text-muted-foreground",
)}
>
annual
</button>
<span className="text-sm px-3 py-1 bg-personality/10 text-personality rounded-full font-600">
Save 17%
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-4xl mx-auto">
{modifiedTiers.map((tier) => (
<PricingCard
key={tier.name}
tier={tier}
billingPeriod={billingPeriod}
onCtaClick={() => handleTierAction(tier.name)}
disabled={processingTier !== null || tier.name === "Starter"}
loading={processingTier === tier.name}
/>
))}
</div>
{/* trust signals */}
<div className="grid md:grid-cols-3 gap-8 w-full border-t pt-16 pb-8 max-w-4xl mx-auto">
<div className="flex flex-col items-center text-center gap-2">
<Icon icon="eyeClosed" iconStyle={"pixel"} className="size-8" color="var(--personality)" />
<p className="font-700">Secure & Encrypted</p>
<p className="text-sm text-muted-foreground">Your data is safe with us</p>
</div>
<div className="flex flex-col items-center text-center gap-2">
<Icon
icon="creditCardDelete"
iconStyle={"pixel"}
className="size-8"
color="var(--personality)"
/>
<p className="font-700">Free Starter Plan</p>
<p className="text-sm text-muted-foreground">Get started instantly</p>
</div>
<div className="flex flex-col items-center text-center gap-2">
<Icon icon="rotateCcw" iconStyle={"pixel"} className="size-8" color="var(--personality)" />
<p className="font-700">Money Back Guarantee</p>
<p className="text-sm text-muted-foreground">30-day no-risk policy</p>
</div>
</div>
</div>
</main>
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
</div>
);
}