# Free Trial Signup Flow - Complete Fix ## Problem: Complex signup with plan selection; Need: Simple free trial --- ## Current Flow Analysis ### Frontend ([`SignUpForm.tsx`](frontend/src/components/auth/SignUpForm.tsx)) **Problems:** 1. Lines 29-64: Loads all plans from API 2. Lines 257-279: Shows plan selection dropdown (required) 3. Line 85-88: Validates plan is selected 4. Line 101: Passes `plan_id` to register 5. Line 105: Redirects to `/account/plans` after signup (payment/plan page) ### Backend ([`RegisterSerializer`](backend/igny8_core/auth/serializers.py:276)) **Problems:** 1. Line 282-290: If no plan_id, tries to find 'free' plan or cheapest 2. Line 332-337: Creates account but **NO credit seeding** 3. No default status='trial' set 4. No automatic trial period setup ### Current User Journey (Messy) ``` Marketing → Click "Sign Up" → /signup page loads plans API → User must select plan from dropdown → Submit registration with plan_id → Backend creates account (0 credits!) → Redirect to /account/plans (payment page) → User confused, no clear trial ``` --- ## Solution: Simple Free Trial Signup ### Desired User Journey (Clean) ``` Marketing → Click "Sign Up" → /signup page (no plan selection!) → User fills: name, email, password → Submit → Backend auto-assigns "Free Trial" plan → Credits seeded automatically → Status = 'trial' → Redirect to /sites (dashboard) → User starts using app immediately ``` --- ## Implementation Steps ### Step 1: Create Free Trial Plan (Database) Run in Django shell or create migration: ```python from igny8_core.auth.models import Plan # Create or update Free Trial plan Plan.objects.update_or_create( slug='free-trial', defaults={ 'name': 'Free Trial', 'price': 0.00, 'billing_cycle': 'monthly', 'included_credits': 2000, # Enough for testing 'max_sites': 1, 'max_users': 1, 'max_industries': 3, 'is_active': True, 'features': ['ai_writer', 'planner', 'basic_support'] } ) ``` **Verify:** ```bash python manage.py shell >>> from igny8_core.auth.models import Plan >>> Plan.objects.get(slug='free-trial') ``` --- ### Step 2: Update Backend Registration to Auto-Assign Free Trial **File:** [`backend/igny8_core/auth/serializers.py:276`](backend/igny8_core/auth/serializers.py:276) **Current code (lines 280-343):** Has issues - no credits, tries to find plan **Replace with:** ```python def create(self, validated_data): from django.db import transaction from igny8_core.business.billing.models import CreditTransaction with transaction.atomic(): # ALWAYS assign Free Trial plan for /signup route # Ignore plan_id if provided - this route is for free trials only try: plan = Plan.objects.get(slug='free-trial', is_active=True) except Plan.DoesNotExist: # Fallback to 'free' if free-trial doesn't exist try: plan = Plan.objects.get(slug='free', is_active=True) except Plan.DoesNotExist: raise serializers.ValidationError({ "plan": "Free trial plan not configured. Please contact support." }) # Generate account name account_name = validated_data.get('account_name') if not account_name: first_name = validated_data.get('first_name', '') last_name = validated_data.get('last_name', '') if first_name or last_name: account_name = f"{first_name} {last_name}".strip() or \ validated_data['email'].split('@')[0] else: account_name = validated_data['email'].split('@')[0] # Generate username if not provided username = validated_data.get('username') if not username: username = validated_data['email'].split('@')[0] base_username = username counter = 1 while User.objects.filter(username=username).exists(): username = f"{base_username}{counter}" counter += 1 # Create user first user = User.objects.create_user( username=username, email=validated_data['email'], password=validated_data['password'], first_name=validated_data.get('first_name', ''), last_name=validated_data.get('last_name', ''), account=None, role='owner' ) # Create account with unique slug base_slug = account_name.lower().replace(' ', '-').replace('_', '-')[:50] or 'account' slug = base_slug counter = 1 while Account.objects.filter(slug=slug).exists(): slug = f"{base_slug}-{counter}" counter += 1 # Get trial credits from plan trial_credits = plan.get_effective_credits_per_month() account = Account.objects.create( name=account_name, slug=slug, owner=user, plan=plan, credits=trial_credits, # CRITICAL: Seed credits status='trial' # CRITICAL: Set as trial account ) # Log initial credit transaction CreditTransaction.objects.create( account=account, transaction_type='subscription', amount=trial_credits, balance_after=trial_credits, description=f'Free trial credits from {plan.name}', metadata={ 'plan_slug': plan.slug, 'registration': True, 'trial': True } ) # Link user to account user.account = account user.save() return user ``` **Changes:** - Line 283: Force free-trial plan, ignore plan_id - Line 352: Set `credits=trial_credits` - Line 353: Set `status='trial'` - Lines 356-365: Log credit transaction --- ### Step 3: Update Frontend to Remove Plan Selection **File:** [`frontend/src/components/auth/SignUpForm.tsx`](frontend/src/components/auth/SignUpForm.tsx) **Remove lines 29-64** (plan loading logic) **Remove lines 257-279** (plan selection dropdown) **Remove lines 85-88** (plan validation) **Remove line 101** (plan_id from register call) **Replace handleSubmit (lines 71-115):** ```typescript const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); if (!formData.email || !formData.password || !formData.firstName || !formData.lastName) { setError("Please fill in all required fields"); return; } if (!isChecked) { setError("Please agree to the Terms and Conditions"); return; } try { // Generate username from email if not provided const username = formData.username || formData.email.split("@")[0]; // NO plan_id - backend will auto-assign free trial await register({ email: formData.email, password: formData.password, username: username, first_name: formData.firstName, last_name: formData.lastName, account_name: formData.accountName, }); // Redirect to dashboard/sites instead of payment page navigate("/sites", { replace: true }); } catch (err: any) { setError(err.message || "Registration failed. Please try again."); } }; ``` **Remove state:** ```typescript // DELETE these lines: const [plans, setPlans] = useState([]); const [selectedPlanId, setSelectedPlanId] = useState(null); const [plansLoading, setPlansLoading] = useState(true); ``` **Remove useEffect** (lines 41-64 - plan loading) --- ### Step 4: Update Auth Store **File:** [`frontend/src/store/authStore.ts:120`](frontend/src/store/authStore.ts:120) No changes needed - it already handles registration without plan_id correctly. --- ### Step 5: Update Middleware to Allow 'trial' Status **File:** [`backend/igny8_core/auth/middleware.py:132`](backend/igny8_core/auth/middleware.py:132) Ensure trial accounts can login - current code should already allow this. Check validation logic allows status='trial': ```python # In validate_account_and_plan helper (to be created) # Allow 'trial' status along with 'active' if account.status in ['suspended', 'cancelled']: # Block only suspended/cancelled # Allow: 'trial', 'active', 'pending_payment' return (False, f'Account is {account.status}', 403) ``` --- ## Complete Code Changes ### Change 1: Update RegisterSerializer **File:** `backend/igny8_core/auth/serializers.py` Replace lines 276-343 with: ```python def create(self, validated_data): from django.db import transaction from igny8_core.business.billing.models import CreditTransaction with transaction.atomic(): # ALWAYS assign Free Trial plan for simple signup # Ignore plan_id parameter - this is for free trial signups only try: plan = Plan.objects.get(slug='free-trial', is_active=True) except Plan.DoesNotExist: try: plan = Plan.objects.get(slug='free', is_active=True) except Plan.DoesNotExist: raise serializers.ValidationError({ "plan": "Free trial plan not configured. Please contact support." }) # Generate account name if not provided account_name = validated_data.get('account_name') if not account_name: first_name = validated_data.get('first_name', '') last_name = validated_data.get('last_name', '') if first_name or last_name: account_name = f"{first_name} {last_name}".strip() or \ validated_data['email'].split('@')[0] else: account_name = validated_data['email'].split('@')[0] # Generate username if not provided username = validated_data.get('username') if not username: username = validated_data['email'].split('@')[0] base_username = username counter = 1 while User.objects.filter(username=username).exists(): username = f"{base_username}{counter}" counter += 1 # Create user first without account user = User.objects.create_user( username=username, email=validated_data['email'], password=validated_data['password'], first_name=validated_data.get('first_name', ''), last_name=validated_data.get('last_name', ''), account=None, role='owner' ) # Generate unique slug for account base_slug = account_name.lower().replace(' ', '-').replace('_', '-')[:50] or 'account' slug = base_slug counter = 1 while Account.objects.filter(slug=slug).exists(): slug = f"{base_slug}-{counter}" counter += 1 # Get trial credits from plan trial_credits = plan.get_effective_credits_per_month() # Create account with trial status and credits account = Account.objects.create( name=account_name, slug=slug, owner=user, plan=plan, credits=trial_credits, status='trial' ) # Log initial credit transaction CreditTransaction.objects.create( account=account, transaction_type='subscription', amount=trial_credits, balance_after=trial_credits, description=f'Free trial credits from {plan.name}', metadata={ 'plan_slug': plan.slug, 'registration': True, 'trial': True } ) # Update user to reference account user.account = account user.save() return user ``` ### Change 2: Simplify SignUpForm **File:** `frontend/src/components/auth/SignUpForm.tsx` Replace entire component with: ```typescript import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "../../icons"; import Label from "../form/Label"; import Input from "../form/input/InputField"; import Checkbox from "../form/input/Checkbox"; import { useAuthStore } from "../../store/authStore"; export default function SignUpForm() { const [showPassword, setShowPassword] = useState(false); const [isChecked, setIsChecked] = useState(false); const [formData, setFormData] = useState({ firstName: "", lastName: "", email: "", password: "", username: "", accountName: "", }); const [error, setError] = useState(""); const navigate = useNavigate(); const { register, loading } = useAuthStore(); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); if (!formData.email || !formData.password || !formData.firstName || !formData.lastName) { setError("Please fill in all required fields"); return; } if (!isChecked) { setError("Please agree to the Terms and Conditions"); return; } try { const username = formData.username || formData.email.split("@")[0]; // No plan_id needed - backend auto-assigns free trial await register({ email: formData.email, password: formData.password, username: username, first_name: formData.firstName, last_name: formData.lastName, account_name: formData.accountName, }); // Redirect to dashboard/sites instead of payment page navigate("/sites", { replace: true }); } catch (err: any) { setError(err.message || "Registration failed. Please try again."); } }; return (
Back to dashboard

Start Your Free Trial

No credit card required. 2,000 AI credits to get started.

{error && (
{error}
)}
setShowPassword(!showPassword)} className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2" > {showPassword ? ( ) : ( )}

By creating an account means you agree to the{" "} Terms and Conditions, {" "} and our{" "} Privacy Policy

Already have an account?{" "} Sign In

); } ``` **Key changes:** - Removed all plan-related code - Changed heading to "Start Your Free Trial" - Added "No credit card required" subtext - Changed button text to "Start Free Trial" - Redirect to `/sites` instead of `/account/plans` - No plan_id sent to backend --- ## Verification Steps ### 1. Create Free Trial Plan ```bash python manage.py shell >>> from igny8_core.auth.models import Plan >>> Plan.objects.create( slug='free-trial', name='Free Trial', price=0.00, billing_cycle='monthly', included_credits=2000, max_sites=1, max_users=1, max_industries=3, is_active=True ) >>> exit() ``` ### 2. Test Registration Flow ```bash # Visit https://app.igny8.com/signup # Fill form: name, email, password # Submit # Should: # 1. Create account with status='trial' # 2. Set credits=2000 # 3. Redirect to /sites # 4. User can immediately use app ``` ### 3. Verify Database ```bash python manage.py shell >>> from igny8_core.auth.models import User >>> u = User.objects.get(email='test@example.com') >>> u.account.status 'trial' >>> u.account.credits 2000 >>> u.account.plan.slug 'free-trial' ``` --- ## Summary of Changes | File | Action | Lines | |------|--------|-------| | Database | Add free-trial plan | Create via shell/migration | | `auth/serializers.py` | Force free-trial plan, seed credits | 276-343 (68 lines) | | `auth/SignUpForm.tsx` | Remove plan selection, simplify | 29-279 (removed ~80 lines) | **Result:** Clean, simple free trial signup with zero payment friction. --- ## Before vs After ### Before (Messy) ``` User → Signup page → Must select plan → Submit with plan_id → Account created with 0 credits → Redirect to /account/plans (payment) → Confused user ``` ### After (Clean) ``` User → Signup page → Fill name, email, password → Submit (no plan selection) → Account created with: - status='trial' - plan='free-trial' - credits=2000 → Redirect to /sites → User starts using app immediately ``` --- ## Next: Paid Plans (Future) For users who want paid plans: - Create separate `/pricing` page - After selecting paid plan, route to `/signup?plan=growth` - Backend checks query param and assigns that plan instead - OR keep /signup as free trial only and create `/subscribe` for paid **For now: /signup = 100% free trial, zero friction.**