diff --git a/backend/igny8_core/auth/serializers.py b/backend/igny8_core/auth/serializers.py index 6b5a7201..1a72a3db 100644 --- a/backend/igny8_core/auth/serializers.py +++ b/backend/igny8_core/auth/serializers.py @@ -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 diff --git a/frontend/src/components/auth/SignUpFormEnhanced.tsx b/frontend/src/components/auth/SignUpFormEnhanced.tsx index 61bdc9b6..be77fb16 100644 --- a/frontend/src/components/auth/SignUpFormEnhanced.tsx +++ b/frontend/src/components/auth/SignUpFormEnhanced.tsx @@ -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(() => { diff --git a/frontend/src/components/auth/SignUpFormUnified.tsx b/frontend/src/components/auth/SignUpFormUnified.tsx index 58f76c2d..db7c8e8d 100644 --- a/frontend/src/components/auth/SignUpFormUnified.tsx +++ b/frontend/src/components/auth/SignUpFormUnified.tsx @@ -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({ - Save {annualDiscountPercent}% + Save up to {annualDiscountPercent}%
-
+

Sign Up for {selectedPlan?.name || 'IGNY8'}

@@ -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({ - Save {annualDiscountPercent}% + Save up to {annualDiscountPercent}%

{/* Plan Cards - Single column, stacked vertically */} -
+
{plans.map((plan) => { const displayPrice = getDisplayPrice(plan); const features = extractFeatures(plan); diff --git a/frontend/src/components/billing/BillingBalancePanel.tsx b/frontend/src/components/billing/BillingBalancePanel.tsx index 45b2ab7b..07891315 100644 --- a/frontend/src/components/billing/BillingBalancePanel.tsx +++ b/frontend/src/components/billing/BillingBalancePanel.tsx @@ -78,8 +78,8 @@ export default function BillingBalancePanel() {

Status

- - {(balance as any)?.subscription_status || 'No subscription'} + + {(balance as any)?.subscription_status === 'trial' ? 'Active (Trial)' : ((balance as any)?.subscription_status || 'No subscription')}

Subscription status

diff --git a/frontend/src/components/dashboard/AccountInfoWidget.tsx b/frontend/src/components/dashboard/AccountInfoWidget.tsx index c1bf6bcb..7ae85068 100644 --- a/frontend/src/components/dashboard/AccountInfoWidget.tsx +++ b/frontend/src/components/dashboard/AccountInfoWidget.tsx @@ -117,10 +117,10 @@ export default function AccountInfoWidget({ {subscription?.status && ( - {subscription.status} + {subscription.status === 'trial' ? 'Active (Trial)' : subscription.status} )}
diff --git a/frontend/src/pages/AuthPages/SignUp.tsx b/frontend/src/pages/AuthPages/SignUp.tsx index e1d20107..112b7ba6 100644 --- a/frontend/src/pages/AuthPages/SignUp.tsx +++ b/frontend/src/pages/AuthPages/SignUp.tsx @@ -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 */} -
- {/* GridShape Background */} -
-
-
- - - - - - - - -
-
+
+ {/* GridShape Background - Same as signin page */} +
+
{/* Logo - Top Right - Smaller */} - + IGNY8 -
+
{/* Pricing Plans Component Will Load Here */}
diff --git a/frontend/src/pages/account/PlansAndBillingPage.tsx b/frontend/src/pages/account/PlansAndBillingPage.tsx index 848e074e..cd14aea8 100644 --- a/frontend/src/pages/account/PlansAndBillingPage.tsx +++ b/frontend/src/pages/account/PlansAndBillingPage.tsx @@ -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');