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') == '':
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')
paid_plans = ['starter', 'growth', 'scale']
if plan_slug and plan_slug in paid_plans:
if plan_slug:
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
if not attrs.get('billing_country'):
raise serializers.ValidationError({
@@ -348,27 +356,36 @@ class RegisterSerializer(serializers.Serializer):
with transaction.atomic():
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:
plan = Plan.objects.get(slug=plan_slug, is_active=True)
except Plan.DoesNotExist:
raise serializers.ValidationError({
"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'
initial_credits = 0
billing_period_start = timezone.now()
# simple monthly cycle; if annual needed, extend here
billing_period_end = billing_period_start + timedelta(days=30)
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'
initial_credits = plan.get_effective_credits_per_month()
billing_period_start = None

View File

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

View File

@@ -289,7 +289,7 @@ export default function SignUpFormUnified({
size="sm"
onClick={() => setBillingPeriod('monthly')}
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
@@ -300,7 +300,7 @@ export default function SignUpFormUnified({
size="sm"
onClick={() => setBillingPeriod('annually')}
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
@@ -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">
<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>
Save {annualDiscountPercent}%
Save up to {annualDiscountPercent}%
</span>
</div>
</div>
<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">
<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">
@@ -502,7 +502,7 @@ export default function SignUpFormUnified({
size="sm"
onClick={() => setBillingPeriod('monthly')}
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
@@ -513,7 +513,7 @@ export default function SignUpFormUnified({
size="sm"
onClick={() => setBillingPeriod('annually')}
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
@@ -525,12 +525,12 @@ export default function SignUpFormUnified({
<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" />
</svg>
Save {annualDiscountPercent}%
Save up to {annualDiscountPercent}%
</p>
</div>
{/* 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) => {
const displayPrice = getDisplayPrice(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>
</div>
<div className="mt-2">
<Badge variant="light" color={(balance as any)?.subscription_status === 'active' ? 'success' : 'secondary'} className="text-base font-semibold">
{(balance as any)?.subscription_status || 'No subscription'}
<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 === 'trial' ? 'Active (Trial)' : ((balance as any)?.subscription_status || 'No subscription')}
</Badge>
</div>
<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 && (
<Badge
variant="soft"
color={subscription.status === 'active' ? 'success' : 'warning'}
color={(subscription.status === 'active' || subscription.status === 'trial') ? 'success' : 'warning'}
size="sm"
>
{subscription.status}
{subscription.status === 'trial' ? 'Active (Trial)' : subscription.status}
</Badge>
)}
</div>

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import PageMeta from "../../components/common/PageMeta";
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
import GridShape from "../../components/common/GridShape";
interface Plan {
id: number;
@@ -89,25 +90,14 @@ export default function SignUp() {
/>
{/* 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">
{/* GridShape Background */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative flex items-center justify-center z-1 w-full h-full">
<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 className="hidden lg:flex lg:w-1/2 bg-brand-950 dark:bg-white/5 items-center justify-center relative">
{/* GridShape Background - Same as signin page */}
<div className="absolute inset-0 z-0">
<GridShape />
</div>
{/* 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
src="/images/logo/IGNY8_DARK_LOGO.png"
alt="IGNY8"
@@ -115,7 +105,7 @@ export default function SignUp() {
/>
</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 */}
<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
const accountStatus = user?.account?.status || '';
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
&& currentPlan?.slug !== 'free'
&& !hasPendingInvoice;
const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');