diff --git a/src/app/(app)/settings/page.tsx b/src/app/(app)/settings/page.tsx index 5e87a7e..27d634f 100644 --- a/src/app/(app)/settings/page.tsx +++ b/src/app/(app)/settings/page.tsx @@ -7,6 +7,7 @@ import { useTheme } from "@/app/ThemeProvider"; import { useFamily } from "@/app/FamilyProvider"; import { Button, Card, Input, Select, Badge } from "@/components/ui"; import { StorageMeter, MemberLimitBanner } from "@/components/StorageMeter"; +import { UpgradeButton } from "@/components/UpgradeButton"; import { fmtDate } from "@/lib/date-ist"; interface Member { @@ -45,6 +46,21 @@ export default function SettingsPage() { const [pedEditing, setPedEditing] = useState(false); const [pedSaved, setPedSaved] = useState(false); const [pedError, setPedError] = useState(""); + const [cancelling, setCancelling] = useState(false); + const [cancelMsg, setCancelMsg] = useState(""); + + const handleCancelSubscription = async () => { + if (!window.confirm("Cancel your subscription? You'll keep premium until the end of your current billing period.")) return; + setCancelling(true); + try { + const res = await fetch("/api/subscriptions/cancel", { method: "POST" }); + const data = await res.json(); + setCancelMsg(res.ok ? data.message : (data.error || "Could not cancel.")); + } catch { + setCancelMsg("Could not reach the server. Try again."); + } + setCancelling(false); + }; // Check if can invite more members (client-side pre-check; server enforces) const canInvite = tier === "pro" || memberCount < 2; @@ -192,6 +208,45 @@ export default function SettingsPage() {
+ {/* Plan / Upgrade — anchor target for all "upgrade" CTAs across the app */} +
+
+

+ {tier === "pro" ? "✨ Tia Premium" : "Your Plan"} +

+ + {tier === "pro" ? "Premium" : "Free"} + +
+ + {tier === "pro" ? ( + <> +

+ 50 GB storage · up to 6 members · up to 3 baby profiles +

+ {cancelMsg ? ( +

{cancelMsg}

+ ) : ( + + )} + + ) : ( + <> +

+ Upgrade for 50 GB media storage, up to 6 family members, + and 3 baby profiles. ₹199/month, cancel anytime. +

+ + + )} +
+ {/* My Profile Page */} @@ -244,7 +299,9 @@ export default function SettingsPage() {
{tier === "free" && ( - + + + )} diff --git a/src/components/UpgradeButton.tsx b/src/components/UpgradeButton.tsx new file mode 100644 index 0000000..9eec903 --- /dev/null +++ b/src/components/UpgradeButton.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { useEffect, useState } from "react"; + +// Minimal shape of the Razorpay Checkout global we use. +interface RazorpayOptions { + key: string; + subscription_id: string; + name: string; + description: string; + handler: (resp: RazorpayResponse) => void; + prefill?: { email?: string; contact?: string }; + theme?: { color?: string }; + modal?: { ondismiss?: () => void }; +} +interface RazorpayResponse { + razorpay_payment_id: string; + razorpay_subscription_id: string; + razorpay_signature: string; +} +interface RazorpayInstance { + open: () => void; + on: (event: string, cb: (resp: unknown) => void) => void; +} +declare global { + interface Window { + Razorpay?: new (options: RazorpayOptions) => RazorpayInstance; + } +} + +const CHECKOUT_SRC = "https://checkout.razorpay.com/v1/checkout.js"; + +function loadCheckout(): Promise { + return new Promise((resolve) => { + if (window.Razorpay) return resolve(true); + const existing = document.querySelector(`script[src="${CHECKOUT_SRC}"]`); + if (existing) { + existing.addEventListener("load", () => resolve(true)); + existing.addEventListener("error", () => resolve(false)); + return; + } + const s = document.createElement("script"); + s.src = CHECKOUT_SRC; + s.onload = () => resolve(true); + s.onerror = () => resolve(false); + document.body.appendChild(s); + }); +} + +export function UpgradeButton({ + email, + className, + label = "Upgrade to Premium — ₹199/month", + onActivating, +}: { + email?: string; + className?: string; + label?: string; + onActivating?: () => void; +}) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [done, setDone] = useState(false); + + // Warm the checkout script so the first click is instant. + useEffect(() => { loadCheckout(); }, []); + + const handleUpgrade = async () => { + setLoading(true); + setError(null); + try { + const ok = await loadCheckout(); + if (!ok || !window.Razorpay) throw new Error("Could not load payment checkout. Check your connection."); + + // 1. Create the subscription server-side. + const createRes = await fetch("/api/subscriptions/create", { method: "POST" }); + const createData = await createRes.json(); + if (!createRes.ok) throw new Error(createData.error || "Could not start checkout."); + + const { subscriptionId, keyId } = createData as { subscriptionId: string; keyId: string }; + + // 2. Open Razorpay Checkout for the mandate. + const rzp = new window.Razorpay({ + key: keyId, // id only — never the secret + subscription_id: subscriptionId, + name: "Tia", + description: "Tia Premium — ₹199/month", + prefill: email ? { email } : undefined, + theme: { color: "#C26B4E" }, // heirloom terracotta + handler: async (resp) => { + // 3. Verify for UX feedback only — entitlement comes via webhook. + try { + await fetch("/api/subscriptions/verify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(resp), + }); + } catch { /* non-fatal — webhook still grants */ } + setDone(true); + onActivating?.(); + }, + modal: { ondismiss: () => setLoading(false) }, + }); + rzp.open(); + } catch (e) { + setError(e instanceof Error ? e.message : "Something went wrong."); + setLoading(false); + } + }; + + if (done) { + return ( +
+ 🎉 Payment received! Your premium is activating — it’ll be live within a minute. +
+ ); + } + + return ( +
+ + {error &&

{error}

} +
+ ); +}