415 lines
15 KiB
TypeScript
415 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { ApiBaseUrl, ApiServerBaseUrl } from "@/utils/baseurl.utils";
|
|
|
|
interface Plan {
|
|
name: string;
|
|
desc: string;
|
|
amountMonthly: number;
|
|
amountYearly: number;
|
|
features: string[];
|
|
planIdMonthly: string;
|
|
planIdYearly: string;
|
|
popular?: boolean;
|
|
}
|
|
|
|
const ComponentsPricingTableToggle = () => {
|
|
const [billingCycle, setBillingCycle] =
|
|
useState<"monthly" | "yearly">("monthly");
|
|
|
|
const [loadingPlan, setLoadingPlan] = useState<string | null>(null);
|
|
|
|
// Trial State
|
|
const [trialStatus, setTrialStatus] = useState<{
|
|
hasUsedTrial: boolean;
|
|
isTrialActive: boolean;
|
|
trialEndsAt: string | null;
|
|
stripeSessionId: string | null;
|
|
} | null>(null);
|
|
|
|
const [timeLeft, setTimeLeft] = useState("");
|
|
|
|
// Fetch subscription status on mount
|
|
React.useEffect(() => {
|
|
const fetchStatus = async () => {
|
|
const userId = localStorage.getItem("userId");
|
|
if (!userId) return;
|
|
|
|
try {
|
|
const res = await fetch(`${ApiServerBaseUrl}/payment/status?userId=${userId}`);
|
|
const data = await res.json();
|
|
setTrialStatus(data);
|
|
} catch (err) {
|
|
console.error("Failed to fetch sub status", err);
|
|
}
|
|
};
|
|
fetchStatus();
|
|
}, []);
|
|
|
|
// Timer Effect
|
|
React.useEffect(() => {
|
|
if (!trialStatus?.isTrialActive || !trialStatus.trialEndsAt) return;
|
|
|
|
const interval = setInterval(() => {
|
|
const now = new Date().getTime();
|
|
const end = new Date(trialStatus.trialEndsAt!).getTime();
|
|
const diff = end - now;
|
|
|
|
if (diff <= 0) {
|
|
clearInterval(interval);
|
|
setTimeLeft("Trial Expired");
|
|
// Refresh status to ensure strict consistency
|
|
window.location.reload();
|
|
} else {
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
setTimeLeft(`${days}d ${hours}h ${minutes}m left`);
|
|
}
|
|
}, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [trialStatus]);
|
|
|
|
const handleCancelTrial = async () => {
|
|
if (!confirm("Are you sure you want to cancel your free trial? You will lose access immediately.")) return;
|
|
|
|
setLoadingPlan("cancel_trial");
|
|
try {
|
|
const res = await fetch(`${ApiServerBaseUrl}/payment/cancel`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ session_id: trialStatus?.stripeSessionId })
|
|
});
|
|
const data = await res.json();
|
|
localStorage.removeItem('payment_session');
|
|
localStorage.removeItem('paymentDetails');
|
|
alert(data.message);
|
|
window.location.reload();
|
|
} catch (err) {
|
|
alert("Failed to cancel trial");
|
|
}
|
|
setLoadingPlan(null);
|
|
};
|
|
|
|
const plans: Plan[] = [
|
|
{
|
|
name: "Basic Buddy",
|
|
desc: "Perfect for individual creators.",
|
|
amountMonthly: 10,
|
|
amountYearly: 100,
|
|
features: [
|
|
"1 Social Profiles",
|
|
"Basic Analytics",
|
|
"Content Scheduling",
|
|
"Standard Support",
|
|
],
|
|
planIdMonthly: "basic_monthly",
|
|
planIdYearly: "basic_yearly",
|
|
},
|
|
{
|
|
name: "Standard Buddy",
|
|
desc: "For growing businesses and influencers.",
|
|
amountMonthly: 50,
|
|
amountYearly: 500,
|
|
features: [
|
|
"10 Social Profiles",
|
|
"Advanced Analytics",
|
|
"AI Content Generation",
|
|
"Priority Support",
|
|
"Team Collaboration (up to 3)",
|
|
],
|
|
popular: true,
|
|
planIdMonthly: "standard_monthly",
|
|
planIdYearly: "standard_yearly",
|
|
},
|
|
{
|
|
name: "Premium Buddy",
|
|
desc: "Ultimate solution for agencies.",
|
|
amountMonthly: 150,
|
|
amountYearly: 1500,
|
|
features: [
|
|
"25 Social Profiles",
|
|
"White Label Reports",
|
|
"Dedicated Account Manager",
|
|
"API Access",
|
|
"Unlimited Team Members",
|
|
],
|
|
planIdMonthly: "premium_monthly",
|
|
planIdYearly: "premium_yearly",
|
|
},
|
|
];
|
|
|
|
const freeTrialPlan: Plan = {
|
|
name: "7-Day Free Trial",
|
|
desc: "Experience SocialBuddy with no commitment. Full access to all features.",
|
|
amountMonthly: 0,
|
|
amountYearly: 0,
|
|
features: [
|
|
"Full Platform Access",
|
|
"All Premium Features",
|
|
"No Credit Card Required",
|
|
"Cancel Anytime",
|
|
],
|
|
planIdMonthly: "free_trial",
|
|
planIdYearly: "free_trial",
|
|
};
|
|
|
|
const handleSubscribe = async (plan: Plan) => {
|
|
const selectedPriceId =
|
|
billingCycle === "monthly" ? plan.planIdMonthly : plan.planIdYearly;
|
|
|
|
setLoadingPlan(plan.name);
|
|
|
|
try {
|
|
const email = localStorage.getItem("user_email");
|
|
const userId: any = localStorage.getItem("userId");
|
|
|
|
if (plan.planIdMonthly === "free_trial") {
|
|
// --- FREE TRIAL FLOW ---
|
|
const res = await fetch(`${ApiServerBaseUrl}/payment/activate-trial`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, userId }),
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
if (data.payment && data.payment.sessionId) {
|
|
localStorage.setItem('payment_session', data.payment.sessionId);
|
|
localStorage.setItem('paymentDetails', JSON.stringify(data.payment));
|
|
}
|
|
alert(data.message);
|
|
window.location.reload();
|
|
} else {
|
|
alert(data.error || "Failed to activate trial");
|
|
}
|
|
|
|
} else {
|
|
// --- STRIPE FLOW ---
|
|
const res = await fetch(
|
|
`${ApiServerBaseUrl}/payment/create-checkout-session`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
email,
|
|
userId,
|
|
planId: selectedPriceId,
|
|
}),
|
|
}
|
|
);
|
|
|
|
const data = await res.json();
|
|
if (data.url) {
|
|
window.location.href = data.url;
|
|
} else {
|
|
alert("Stripe session creation failed.");
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
alert("Payment error — check console.");
|
|
console.error(error);
|
|
}
|
|
|
|
setLoadingPlan(null);
|
|
};
|
|
|
|
return (
|
|
<div className="">
|
|
{/* Billing Toggle */}
|
|
<div className="flex justify-center mb-16">
|
|
<div className="glass-card rounded-full p-1 flex">
|
|
<button
|
|
onClick={() => setBillingCycle("monthly")}
|
|
className={`px-8 py-3 rounded-full text-sm font-bold transition-all ${billingCycle === "monthly"
|
|
? "bg-gradient-to-r from-[#4361ee] to-[#c026d3] text-white"
|
|
: "text-gray-400"
|
|
}`}
|
|
>
|
|
Monthly
|
|
</button>
|
|
<button
|
|
onClick={() => setBillingCycle("yearly")}
|
|
className={`px-8 py-3 rounded-full text-sm font-bold transition-all ${billingCycle === "yearly"
|
|
? "bg-gradient-to-r from-[#4361ee] to-[#c026d3] text-white"
|
|
: "text-gray-400"
|
|
}`}
|
|
>
|
|
Yearly (Save 20%)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Free Trial Banner */}
|
|
<div className="mb-12 w-full p-1 bg-gradient-to-r from-[#4361ee] to-[#c026d3] rounded-3xl animate-fade-in-up">
|
|
<div className="bg-black/90 backdrop-blur-xl rounded-[22px] p-8 md:p-10 flex flex-col md:flex-row items-center justify-between text-center md:text-left gap-6">
|
|
<div className="flex-1">
|
|
<div className="flex items-center justify-center md:justify-start gap-3 mb-2">
|
|
<span className="bg-white/20 text-white text-xs px-3 py-1 rounded-full uppercase tracking-wider font-bold">Limited Offer</span>
|
|
{!(trialStatus?.hasUsedTrial && !trialStatus?.isTrialActive) && (
|
|
<span className="text-yellow-400 text-xs font-bold">⭐ Recommended</span>
|
|
)}
|
|
</div>
|
|
<h3 className="text-3xl md:text-4xl font-bold text-white mb-3">
|
|
{trialStatus?.isTrialActive
|
|
? "Your Free Trial is Active!"
|
|
: trialStatus?.hasUsedTrial
|
|
? "Free Trial Ended"
|
|
: freeTrialPlan.name}
|
|
</h3>
|
|
<p className="text-gray-300 text-lg mb-6">
|
|
{trialStatus?.isTrialActive
|
|
? "Enjoy full access to all premium features. Check your dashboard for insights."
|
|
: trialStatus?.hasUsedTrial
|
|
? "You have already used your free trial. Please select a plan to continue."
|
|
: freeTrialPlan.desc}
|
|
</p>
|
|
|
|
{/* Show features list only if not used or active */}
|
|
{!trialStatus?.isTrialActive && !trialStatus?.hasUsedTrial && (
|
|
<div className="flex flex-wrap gap-3 justify-center md:justify-start">
|
|
{freeTrialPlan.features.map((f, i) => (
|
|
<div key={i} className="flex items-center gap-2 bg-white/5 px-4 py-2 rounded-lg border border-white/10">
|
|
<div className="w-5 h-5 rounded-full bg-green-500/20 text-green-400 flex items-center justify-center text-xs">✓</div>
|
|
<span className="text-sm text-gray-200">{f}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full md:w-auto min-w-[200px]">
|
|
{trialStatus?.isTrialActive ? (
|
|
// --- ACTIVE TRIAL STATE ---
|
|
<div className="text-center">
|
|
<div className="mb-4">
|
|
<p className="text-gray-400 text-sm mb-1">Time Remaining</p>
|
|
<div className="text-2xl font-mono font-bold text-white bg-white/10 py-2 px-4 rounded-lg">
|
|
{timeLeft || "Calculating..."}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleCancelTrial}
|
|
disabled={loadingPlan === "cancel_trial"}
|
|
className="w-full py-3 rounded-xl font-bold text-sm transition-all border border-red-500/50 text-red-400 hover:bg-red-500/10"
|
|
>
|
|
{loadingPlan === "cancel_trial" ? "Cancelling..." : "Cancel Trial"}
|
|
</button>
|
|
<p className="text-center text-xs text-gray-500 mt-2 max-w-[200px] mx-auto">
|
|
Cancelling will end your access immediately.
|
|
</p>
|
|
</div>
|
|
) : trialStatus?.hasUsedTrial ? (
|
|
// --- USED / EXPIRED STATE ---
|
|
<div className="text-center">
|
|
<div className="mb-3">
|
|
<span className="text-3xl font-bold text-gray-400">Used</span>
|
|
</div>
|
|
<button
|
|
disabled
|
|
className="w-full py-4 rounded-xl font-bold text-lg transition-all bg-white/5 text-gray-500 cursor-not-allowed border border-white/5"
|
|
>
|
|
Trial Unavailable
|
|
</button>
|
|
<p className="text-center text-xs text-gray-500 mt-2">One-time offer only.</p>
|
|
</div>
|
|
) : (
|
|
// --- SUBSCRIBE STATE ---
|
|
<>
|
|
<div className="text-center mb-3">
|
|
<span className="text-4xl font-bold text-white">$0</span>
|
|
<span className="text-gray-400"> / 7 days</span>
|
|
</div>
|
|
<button
|
|
onClick={() => handleSubscribe(freeTrialPlan)}
|
|
disabled={loadingPlan === freeTrialPlan.name}
|
|
className="w-full py-4 rounded-xl font-bold text-lg transition-all bg-white text-black hover:bg-gray-100 shadow-[0_0_20px_rgba(255,255,255,0.3)] hover:shadow-[0_0_30px_rgba(255,255,255,0.5)] transform hover:-translate-y-1"
|
|
>
|
|
{loadingPlan === freeTrialPlan.name ? "Processing..." : "Start Free Trial"}
|
|
</button>
|
|
<p className="text-center text-xs text-gray-500 mt-2">No commitment, cancel anytime.</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Pricing Plans */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
{plans.map((plan) => (
|
|
<div
|
|
key={plan.name}
|
|
className={`relative glass-card rounded-3xl p-8 flex flex-col border ${plan.popular ? "border-purple-500/50" : "border-white/10"
|
|
} hover:-translate-y-2 transition-all`}
|
|
>
|
|
{plan.popular && (
|
|
<div className="absolute -top-4 left-1/2 -translate-x-1/2 bg-gradient-to-r from-[#4361ee] to-[#c026d3] text-white px-4 py-1 rounded-full text-xs">
|
|
MOST POPULAR
|
|
</div>
|
|
)}
|
|
|
|
<h3 className="text-2xl font-bold text-white mb-2">{plan.name}</h3>
|
|
<p className="text-gray-400 mb-6 text-sm">{plan.desc}</p>
|
|
|
|
{/* Price */}
|
|
<div className="mb-8">
|
|
<span className="text-5xl font-extrabold text-white">
|
|
$
|
|
{billingCycle === "monthly"
|
|
? plan.amountMonthly
|
|
: plan.amountYearly}
|
|
</span>
|
|
<span className="text-gray-400 text-sm">
|
|
/{billingCycle === "monthly" ? "mo" : "yr"}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Features */}
|
|
<ul className="space-y-4 text-gray-300 text-sm flex-1 mb-10">
|
|
{plan.features.map((f, i) => (
|
|
<li key={i} className="flex items-center gap-3">
|
|
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-[#4361ee] to-[#c026d3] flex items-center justify-center p-[1px]">
|
|
<div className="bg-black w-full h-full rounded-full flex items-center justify-center">
|
|
<svg
|
|
className="w-3.5 h-3.5 text-white"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={3}
|
|
d="M5 13l4 4L19 7"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
{f}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{/* FIXED BUTTON */}
|
|
<button
|
|
onClick={() => handleSubscribe(plan)}
|
|
disabled={loadingPlan === plan.name}
|
|
className="w-full py-4 rounded-xl font-bold transition-all
|
|
bg-gradient-to-r from-[#4361ee] to-[#c026d3]
|
|
text-white hover:opacity-90"
|
|
>
|
|
{loadingPlan === plan.name ? "Processing..." : "Get Started"}
|
|
</button>
|
|
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ComponentsPricingTableToggle;
|