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}
}
+
+ );
+}