free and trial plans fixes and styling of sigini and signup forms

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-17 06:12:47 +00:00
parent 839260a7db
commit 491ddc5fbb
7 changed files with 51 additions and 44 deletions

View File

@@ -320,10 +320,18 @@ class RegisterSerializer(serializers.Serializer):
if 'plan_id' in attrs and attrs.get('plan_id') == '': if 'plan_id' in attrs and attrs.get('plan_id') == '':
attrs['plan_id'] = None attrs['plan_id'] = None
# Validate billing fields for paid plans # Validate billing fields for paid plans (check by price, not hardcoded slugs)
plan_slug = attrs.get('plan_slug') plan_slug = attrs.get('plan_slug')
paid_plans = ['starter', 'growth', 'scale'] if plan_slug:
if plan_slug and plan_slug in paid_plans: try:
plan = Plan.objects.get(slug=plan_slug, is_active=True)
is_paid_plan = plan.price > 0
except Plan.DoesNotExist:
raise serializers.ValidationError({"plan": f"Plan '{plan_slug}' not found."})
else:
is_paid_plan = False
if is_paid_plan:
# Require billing_country for paid plans # Require billing_country for paid plans
if not attrs.get('billing_country'): if not attrs.get('billing_country'):
raise serializers.ValidationError({ raise serializers.ValidationError({
@@ -348,27 +356,36 @@ class RegisterSerializer(serializers.Serializer):
with transaction.atomic(): with transaction.atomic():
plan_slug = validated_data.get('plan_slug') plan_slug = validated_data.get('plan_slug')
paid_plans = ['starter', 'growth', 'scale']
if plan_slug and plan_slug in paid_plans: # Fetch plan and determine paid/free by price, not hardcoded slugs
if plan_slug:
try: try:
plan = Plan.objects.get(slug=plan_slug, is_active=True) plan = Plan.objects.get(slug=plan_slug, is_active=True)
except Plan.DoesNotExist: except Plan.DoesNotExist:
raise serializers.ValidationError({ raise serializers.ValidationError({
"plan": f"Plan '{plan_slug}' not available. Please contact support." "plan": f"Plan '{plan_slug}' not available. Please contact support."
}) })
else:
# No plan_slug provided, get first free plan (price=0)
try:
plan = Plan.objects.filter(is_active=True, price=0).first()
if not plan:
raise Plan.DoesNotExist
except Plan.DoesNotExist:
raise serializers.ValidationError({
"plan": "No free plan available. Please contact support."
})
# Determine account status based on plan price
is_paid_plan = plan.price > 0
if is_paid_plan:
account_status = 'pending_payment' account_status = 'pending_payment'
initial_credits = 0 initial_credits = 0
billing_period_start = timezone.now() billing_period_start = timezone.now()
# simple monthly cycle; if annual needed, extend here # simple monthly cycle; if annual needed, extend here
billing_period_end = billing_period_start + timedelta(days=30) billing_period_end = billing_period_start + timedelta(days=30)
else: else:
try:
plan = Plan.objects.get(slug='free', is_active=True)
except Plan.DoesNotExist:
raise serializers.ValidationError({
"plan": "Free plan not configured. Please contact support."
})
account_status = 'trial' account_status = 'trial'
initial_credits = plan.get_effective_credits_per_month() initial_credits = plan.get_effective_credits_per_month()
billing_period_start = None billing_period_start = None

View File

@@ -60,8 +60,8 @@ export default function SignUpFormEnhanced({ planDetails: planDetailsProp, planL
const { register, loading } = useAuthStore(); const { register, loading } = useAuthStore();
const planSlug = new URLSearchParams(window.location.search).get('plan') || ''; const planSlug = new URLSearchParams(window.location.search).get('plan') || '';
const paidPlans = ['starter', 'growth', 'scale']; // Determine if plan is paid based on price, not hardcoded slug
const isPaidPlan = planSlug && paidPlans.includes(planSlug); const isPaidPlan = planDetails && parseFloat(String(planDetails.price || 0)) > 0;
const totalSteps = isPaidPlan ? 3 : 1; const totalSteps = isPaidPlan ? 3 : 1;
useEffect(() => { useEffect(() => {

View File

@@ -289,7 +289,7 @@ export default function SignUpFormUnified({
size="sm" size="sm"
onClick={() => setBillingPeriod('monthly')} onClick={() => setBillingPeriod('monthly')}
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${ className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
billingPeriod === 'monthly' ? 'text-white hover:text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200' billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-700'
}`} }`}
> >
Monthly Monthly
@@ -300,7 +300,7 @@ export default function SignUpFormUnified({
size="sm" size="sm"
onClick={() => setBillingPeriod('annually')} onClick={() => setBillingPeriod('annually')}
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${ className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
billingPeriod === 'annually' ? 'text-white hover:text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200' billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-700'
}`} }`}
> >
Annually Annually
@@ -312,13 +312,13 @@ export default function SignUpFormUnified({
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
Save {annualDiscountPercent}% Save up to {annualDiscountPercent}%
</span> </span>
</div> </div>
</div> </div>
<div className="flex-1 overflow-y-auto no-scrollbar flex items-center"> <div className="flex-1 overflow-y-auto no-scrollbar flex items-center">
<div className="w-full mx-auto p-6 sm:p-8 max-w-2xl"> <div className="w-full mx-auto p-6 sm:p-8 max-w-[572px]">
<div className="mb-6"> <div className="mb-6">
<h1 className="mb-2 font-semibold text-gray-800 dark:text-white text-2xl">Sign Up for {selectedPlan?.name || 'IGNY8'}</h1> <h1 className="mb-2 font-semibold text-gray-800 dark:text-white text-2xl">Sign Up for {selectedPlan?.name || 'IGNY8'}</h1>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
@@ -502,7 +502,7 @@ export default function SignUpFormUnified({
size="sm" size="sm"
onClick={() => setBillingPeriod('monthly')} onClick={() => setBillingPeriod('monthly')}
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${ className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
billingPeriod === 'monthly' ? 'text-white hover:text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200' billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-700'
}`} }`}
> >
Monthly Monthly
@@ -513,7 +513,7 @@ export default function SignUpFormUnified({
size="sm" size="sm"
onClick={() => setBillingPeriod('annually')} onClick={() => setBillingPeriod('annually')}
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${ className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
billingPeriod === 'annually' ? 'text-white hover:text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200' billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-700'
}`} }`}
> >
Annually Annually
@@ -525,12 +525,12 @@ export default function SignUpFormUnified({
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
Save {annualDiscountPercent}% Save up to {annualDiscountPercent}%
</p> </p>
</div> </div>
{/* Plan Cards - Single column, stacked vertically */} {/* Plan Cards - Single column, stacked vertically */}
<div className="grid gap-4 grid-cols-1 w-full max-w-[840px] mx-auto"> <div className="grid gap-4 grid-cols-1 w-full max-w-[640px] mx-auto">
{plans.map((plan) => { {plans.map((plan) => {
const displayPrice = getDisplayPrice(plan); const displayPrice = getDisplayPrice(plan);
const features = extractFeatures(plan); const features = extractFeatures(plan);

View File

@@ -78,8 +78,8 @@ export default function BillingBalancePanel() {
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Status</h3> <h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Status</h3>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<Badge variant="light" color={(balance as any)?.subscription_status === 'active' ? 'success' : 'secondary'} className="text-base font-semibold"> <Badge variant="light" color={((balance as any)?.subscription_status === 'active' || (balance as any)?.subscription_status === 'trial') ? 'success' : 'secondary'} className="text-base font-semibold">
{(balance as any)?.subscription_status || 'No subscription'} {(balance as any)?.subscription_status === 'trial' ? 'Active (Trial)' : ((balance as any)?.subscription_status || 'No subscription')}
</Badge> </Badge>
</div> </div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Subscription status</p> <p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Subscription status</p>

View File

@@ -117,10 +117,10 @@ export default function AccountInfoWidget({
{subscription?.status && ( {subscription?.status && (
<Badge <Badge
variant="soft" variant="soft"
color={subscription.status === 'active' ? 'success' : 'warning'} color={(subscription.status === 'active' || subscription.status === 'trial') ? 'success' : 'warning'}
size="sm" size="sm"
> >
{subscription.status} {subscription.status === 'trial' ? 'Active (Trial)' : subscription.status}
</Badge> </Badge>
)} )}
</div> </div>

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import PageMeta from "../../components/common/PageMeta"; import PageMeta from "../../components/common/PageMeta";
import SignUpFormUnified from "../../components/auth/SignUpFormUnified"; import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
import GridShape from "../../components/common/GridShape";
interface Plan { interface Plan {
id: number; id: number;
@@ -89,25 +90,14 @@ export default function SignUp() {
/> />
{/* Right Side - Pricing Plans */} {/* Right Side - Pricing Plans */}
<div className="hidden lg:flex lg:w-1/2 bg-brand-950 dark:bg-white/5 p-8 xl:p-12 items-center justify-center relative"> <div className="hidden lg:flex lg:w-1/2 bg-brand-950 dark:bg-white/5 items-center justify-center relative">
{/* GridShape Background */} {/* GridShape Background - Same as signin page */}
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 z-0">
<div className="relative flex items-center justify-center z-1 w-full h-full"> <GridShape />
<div className="absolute inset-0">
<svg className="w-full h-full opacity-40" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="0.5"/>
</pattern>
</defs>
<rect width="100" height="100" fill="url(#grid)" />
</svg>
</div>
</div>
</div> </div>
{/* Logo - Top Right - Smaller */} {/* Logo - Top Right - Smaller */}
<Link to="/" className="absolute top-6 right-6 z-10"> <Link to="/" className="absolute top-6 right-6 z-20">
<img <img
src="/images/logo/IGNY8_DARK_LOGO.png" src="/images/logo/IGNY8_DARK_LOGO.png"
alt="IGNY8" alt="IGNY8"
@@ -115,7 +105,7 @@ export default function SignUp() {
/> />
</Link> </Link>
<div className="w-full max-w-[840px] relative z-10"> <div className="w-full max-w-[640px] relative z-10">
{/* Pricing Plans Component Will Load Here */} {/* Pricing Plans Component Will Load Here */}
<div id="signup-pricing-plans" className="w-full"> <div id="signup-pricing-plans" className="w-full">

View File

@@ -643,9 +643,9 @@ export default function PlansAndBillingPage() {
// FIX: hasActivePlan should check account status, not just plan existence // FIX: hasActivePlan should check account status, not just plan existence
const accountStatus = user?.account?.status || ''; const accountStatus = user?.account?.status || '';
const hasPendingInvoice = invoices.some((inv) => inv.status === 'pending'); const hasPendingInvoice = invoices.some((inv) => inv.status === 'pending');
const hasActivePlan = accountStatus === 'active' // Accept both 'active' and 'trial' status (free plans get 'trial' status)
const hasActivePlan = (accountStatus === 'active' || accountStatus === 'trial')
&& effectivePlanId && effectivePlanId
&& currentPlan?.slug !== 'free'
&& !hasPendingInvoice; && !hasPendingInvoice;
const hasPendingPayment = payments.some((p) => p.status === 'pending_approval'); const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');