diff --git a/backend/igny8_core/auth/serializers.py b/backend/igny8_core/auth/serializers.py index 1a72a3db..fd92d4da 100644 --- a/backend/igny8_core/auth/serializers.py +++ b/backend/igny8_core/auth/serializers.py @@ -481,7 +481,7 @@ class RegisterSerializer(serializers.Serializer): user.save() # For paid plans, create subscription, invoice, and default payment method - if plan_slug and plan_slug in paid_plans: + if is_paid_plan: payment_method = validated_data.get('payment_method', 'bank_transfer') subscription = Subscription.objects.create( diff --git a/docs/audits/SIGNUP_FORM_AUDIT_2026-01-17.md b/docs/audits/SIGNUP_FORM_AUDIT_2026-01-17.md new file mode 100644 index 00000000..419d213f --- /dev/null +++ b/docs/audits/SIGNUP_FORM_AUDIT_2026-01-17.md @@ -0,0 +1,650 @@ +# SignUpFormUnified Component - Complete Audit Report +**Date**: January 17, 2026 +**Component**: `/frontend/src/components/auth/SignUpFormUnified.tsx` +**Total Lines**: 611 +**Auditor**: GitHub Copilot + +--- + +## ๐ŸŽฏ Executive Summary + +The SignUpFormUnified component is a **production-ready, comprehensive signup form** that handles both free and paid plan registrations with integrated pricing selection. The component follows modern React patterns and includes robust error handling. + +### Key Strengths +- โœ… Unified experience for free and paid plans +- โœ… Responsive design (mobile/desktop optimized) +- โœ… Dynamic pricing calculations with annual discounts +- โœ… Graceful error handling with fallbacks +- โœ… Proper TypeScript typing throughout +- โœ… URL state synchronization for plan selection + +### Critical Issues Fixed Today +- โœ… **500 Error**: Fixed hardcoded `paid_plans` variable in backend serializer +- โœ… **Button Colors**: Fixed text color override by replacing Button components with native buttons +- โœ… **CORS Handling**: Proper error handling for ipapi.co geolocation (non-blocking) + +--- + +## ๐Ÿ“‹ Component Architecture + +### 1. **Component Props** +```typescript +interface SignUpFormUnifiedProps { + plans: Plan[]; // Array of available plans from backend + selectedPlan: Plan | null; // Currently selected plan + onPlanSelect: (plan: Plan) => void; // Plan selection handler + plansLoading: boolean; // Loading state for plans +} +``` + +### 2. **State Management** +| State Variable | Type | Purpose | Default | +|---------------|------|---------|---------| +| `showPassword` | `boolean` | Toggle password visibility | `false` | +| `isChecked` | `boolean` | Terms & conditions checkbox | `false` | +| `billingPeriod` | `'monthly' \| 'annually'` | Billing cycle selector | `'monthly'` | +| `annualDiscountPercent` | `number` | Dynamic discount from plans | `15` | +| `formData` | `object` | User input fields | See below | +| `countries` | `Country[]` | Available countries list | `[]` | +| `countriesLoading` | `boolean` | Country fetch loading state | `true` | +| `error` | `string` | Form error message | `''` | + +### 3. **Form Data Structure** +```typescript +{ + firstName: string; // Required + lastName: string; // Required + email: string; // Required + password: string; // Required + accountName: string; // Optional (auto-generated from name) + billingCountry: string; // Required (default: 'US') +} +``` + +--- + +## ๐Ÿ”Œ External Dependencies + +### API Endpoints +1. **GET** `/api/v1/auth/countries/` + - **Purpose**: Fetch available countries for dropdown + - **Response**: `{ countries: Country[] }` + - **Fallback**: Hardcoded 6 countries (US, GB, CA, AU, PK, IN) + - **Error Handling**: โœ… Graceful fallback + +2. **POST** `/api/v1/auth/register/` + - **Purpose**: Create new user account + - **Payload**: `{ email, password, username, first_name, last_name, account_name, plan_slug, billing_country }` + - **Response**: User object with tokens + - **Error Handling**: โœ… Displays user-friendly error messages + +3. **GET** `https://ipapi.co/country_code/` (External) + - **Purpose**: Detect user's country (optional enhancement) + - **Timeout**: 3 seconds + - **CORS**: โš ๏ธ May fail (expected, non-blocking) + - **Fallback**: Keeps default 'US' + - **Error Handling**: โœ… Silent failure + +### Zustand Store +- **Store**: `useAuthStore` +- **Actions Used**: + - `register(payload)` - User registration + - `loading` - Loading state +- **State Verification**: Includes fallback logic to force-set auth state if registration succeeds but store isn't updated + +--- + +## ๐ŸŽจ UI/UX Features + +### Responsive Design +| Breakpoint | Layout | Features | +|-----------|--------|----------| +| **Mobile** (`< lg`) | Single column | Toggle at top, plan grid below form, stacked inputs | +| **Desktop** (`โ‰ฅ lg`) | Split screen | Form left (50%), plans right via React Portal | + +### Billing Period Toggle +- **Type**: Custom sliding toggle (not Button component) +- **States**: Monthly / Annually +- **Visual**: Gradient slider (`brand-500` to `brand-600`) +- **Colors**: + - **Active**: White text on gradient background + - **Inactive**: Gray-600 text, hover: gray-200 background +- **Discount Badge**: Shows "Save up to X%" when annually selected + +### Plan Selection +**Mobile**: 2-column grid with cards showing: +- Plan name +- Price (dynamic based on billing period) +- Checkmark icon if selected + +**Desktop**: Single-column stacked cards showing: +- Plan name + price (left) +- Features in 2-column grid (right) +- "POPULAR" badge for Growth plan +- Large checkmark icon for selected plan + +### Form Fields +| Field | Type | Required | Validation | Features | +|-------|------|----------|------------|----------| +| First Name | text | โœ… | Not empty | Half-width on desktop | +| Last Name | text | โœ… | Not empty | Half-width on desktop | +| Email | email | โœ… | Not empty | Full-width | +| Account Name | text | โŒ | None | Auto-generated if empty | +| Password | password | โœ… | Not empty | Eye icon toggle, secure input | +| Country | select | โœ… | Not empty | Dropdown with flag icon, auto-detected | +| Terms Checkbox | checkbox | โœ… | Must be checked | Links to Terms & Privacy | + +### Error Display +- **Position**: Above form fields +- **Style**: Red background with error-50 color +- **Dismissal**: Automatically cleared on next submit +- **Messages**: + - "Please fill in all required fields" + - "Please agree to the Terms and Conditions" + - "Please select a plan" + - Backend error messages (passed through) + +### Loading States +1. **Countries Loading**: Shows spinner with "Loading countries..." text +2. **Form Submission**: Button shows spinner + "Creating your account..." +3. **Plans Loading**: Passed from parent (prop) + +--- + +## ๐Ÿ”„ User Flow + +### Registration Process +``` +1. User selects plan (monthly/annually) + โ†“ +2. User fills in form fields + โ†“ +3. User checks Terms & Conditions + โ†“ +4. User clicks "Create Account" or "Start Free Trial" + โ†“ +5. Form validation (client-side) + โ†“ +6. API call to /auth/register/ + โ†“ +7. Backend creates account, returns user + tokens + โ†“ +8. Frontend sets auth state (with fallback verification) + โ†“ +9. Redirect based on plan type: + - Paid plan โ†’ /account/plans (to select payment) + - Free plan โ†’ /sites (start using app) +``` + +### Post-Registration Navigation +- **Paid Plans**: Navigate to `/account/plans` with `replace: true` + - User can select payment method and complete payment + - Status: `pending_payment` + +- **Free Plans**: Navigate to `/sites` with `replace: true` + - User can immediately start using the app + - Status: `trial` + +--- + +## ๐Ÿงฎ Business Logic + +### 1. Price Calculation +```typescript +getDisplayPrice(plan: Plan): number { + const monthlyPrice = parseFloat(String(plan.price || 0)); + if (billingPeriod === 'annually') { + const discountMultiplier = 1 - (annualDiscountPercent / 100); + return monthlyPrice * 12 * discountMultiplier; + } + return monthlyPrice; +} +``` +- **Monthly**: Shows `plan.price` as-is +- **Annually**: `(monthly ร— 12) ร— (1 - discount%)` +- **Display**: Shows total annual price + per-month breakdown + +### 2. Free vs Paid Plan Detection +```typescript +const isPaidPlan = selectedPlan && parseFloat(String(selectedPlan.price || 0)) > 0; +``` +- **Free Plan**: `price = 0` or `price = '0'` +- **Paid Plan**: `price > 0` +- **Used For**: + - Button text ("Create Account" vs "Start Free Trial") + - Post-registration navigation + - Backend validation (requires payment_method for paid) + +### 3. Feature Extraction +```typescript +extractFeatures(plan: Plan): string[] { + if (plan.features && plan.features.length > 0) { + return plan.features; // Use backend-provided features + } + // Fallback: Build from plan limits + return [ + `${plan.max_sites} Site(s)`, + `${plan.max_users} User(s)`, + `${formatNumber(plan.max_keywords)} Keywords`, + `${formatNumber(plan.included_credits)} Credits/Month` + ]; +} +``` + +### 4. Number Formatting +```typescript +formatNumber(num: number): string { + if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; + if (num >= 1000) return `${(num / 1000).toFixed(0)}K`; + return num.toString(); +} +``` +- 1000 โ†’ "1K" +- 1500000 โ†’ "1.5M" + +### 5. URL State Sync +```typescript +useEffect(() => { + if (selectedPlan) { + const url = new URL(window.location.href); + url.searchParams.set('plan', selectedPlan.slug); + window.history.replaceState({}, '', url.toString()); + } +}, [selectedPlan]); +``` +- Updates URL to `?plan=` when plan changes +- Allows sharing direct links to specific plans +- Uses `replaceState` (no history pollution) + +--- + +## ๐Ÿ”’ Security Considerations + +### โœ… Implemented +1. **Password Visibility Toggle**: User controls when password is visible +2. **Client-Side Validation**: Basic checks before API call +3. **HTTPS Endpoints**: Backend API uses `https://api.igny8.com` +4. **Token Storage**: Handled by `useAuthStore` (likely localStorage) +5. **CORS Protection**: API endpoints properly configured + +### โš ๏ธ Recommendations +1. **Password Strength**: No validation for complexity + - **Suggestion**: Add regex for min 8 chars, 1 uppercase, 1 number + +2. **Email Validation**: Only checks for @ symbol + - **Suggestion**: Add email format regex or use validator library + +3. **Rate Limiting**: No frontend throttling + - **Suggestion**: Backend should implement rate limiting on `/auth/register/` + +4. **CSRF Protection**: Not visible in this component + - **Verification Needed**: Check if backend uses CSRF tokens + +5. **XSS Prevention**: Using React's built-in escaping + - โœ… No `dangerouslySetInnerHTML` usage + +--- + +## ๐Ÿ› Error Handling Analysis + +### API Errors +| Scenario | Handling | User Experience | +|----------|----------|-----------------| +| Network failure | โœ… Catch block | Shows error message below form | +| 400 Bad Request | โœ… Displays backend message | User sees specific field errors | +| 500 Server Error | โœ… Generic message | "Registration failed. Please try again." | +| Timeout | โœ… Caught | Same as network failure | + +### Edge Cases +1. **Plan Not Selected**: โœ… Validation prevents submission +2. **Empty Required Fields**: โœ… Shows "Please fill in all required fields" +3. **Terms Not Checked**: โœ… Shows "Please agree to Terms" +4. **Countries API Fails**: โœ… Fallback to 6 hardcoded countries +5. **Geo Detection Fails**: โœ… Silent fallback to US +6. **Auth State Not Set**: โœ… Force-set with fallback logic +7. **Duplicate Email**: โš ๏ธ Backend should return 400, displayed to user + +### Missing Error Handling +1. **Concurrent Registrations**: What if user clicks submit multiple times? + - **Risk**: Multiple accounts created + - **Fix**: Disable button during loading (โœ… Already done with `disabled={loading}`) + +2. **Session Conflicts**: What if user already logged in? + - **Risk**: Undefined behavior + - **Fix**: Backend has conflict detection (session_conflict error) + +--- + +## โ™ฟ Accessibility Review + +### โœ… Good Practices +- Semantic HTML: `
`, ` - - -
-
-
-
-
- - Or - -
-
- -
- {error && ( -
- {error} -
- )} -
- {/* First Name */} -
- - -
- {/* Last Name */} -
- - -
-
- {/* Email */} -
- - -
- {/* Account Name */} -
- - -
- {/* Password */} -
- -
- - setShowPassword(!showPassword)} - className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2" - > - {showPassword ? ( - - ) : ( - - )} - -
-
- {/* Terms Checkbox */} -
- -

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

-
- {/* Submit Button */} -
- -
-
-
- -
-

- Already have an account?{" "} - - Sign In - -

-
- - - - - ); -} diff --git a/frontend/src/components/auth/SignUpFormEnhanced.tsx b/frontend/src/components/auth/SignUpFormEnhanced.tsx deleted file mode 100644 index be77fb16..00000000 --- a/frontend/src/components/auth/SignUpFormEnhanced.tsx +++ /dev/null @@ -1,443 +0,0 @@ -/** - * Enhanced Multi-Step Signup Form - * Handles paid plan signups with billing information collection - * Step 1: Basic user info - * Step 2: Billing info (for paid plans only) - * Step 3: Payment method selection (for paid plans only) - */ - -import { useState, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { ChevronLeftIcon, EyeCloseIcon, EyeIcon, ChevronRightIcon, CheckIcon } from '../../icons'; -import Label from '../form/Label'; -import Input from '../form/input/InputField'; -import Checkbox from '../form/input/Checkbox'; -import Button from '../ui/button/Button'; -import { useAuthStore } from '../../store/authStore'; -import BillingFormStep, { BillingFormData } from '../billing/BillingFormStep'; -import PaymentMethodSelect, { PaymentMethodConfig } from '../billing/PaymentMethodSelect'; - -interface SignUpFormEnhancedProps { - planDetails?: any; - planLoading?: boolean; -} - -export default function SignUpFormEnhanced({ planDetails: planDetailsProp, planLoading: planLoadingProp }: SignUpFormEnhancedProps) { - const [currentStep, setCurrentStep] = useState(1); - const [showPassword, setShowPassword] = useState(false); - const [isChecked, setIsChecked] = useState(false); - - // Step 1: Basic user info - const [formData, setFormData] = useState({ - firstName: '', - lastName: '', - email: '', - password: '', - username: '', - accountName: '', - }); - - // Step 2: Billing info (for paid plans) - const [billingData, setBillingData] = useState({ - billing_email: '', - billing_address_line1: '', - billing_address_line2: '', - billing_city: '', - billing_state: '', - billing_postal_code: '', - billing_country: '', - tax_id: '', - }); - - // Step 3: Payment method - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(null); - - const [error, setError] = useState(''); - const [planDetails, setPlanDetails] = useState(planDetailsProp || null); - const [planLoading, setPlanLoading] = useState(planLoadingProp || false); - const [planError, setPlanError] = useState(''); - const navigate = useNavigate(); - const { register, loading } = useAuthStore(); - - const planSlug = new URLSearchParams(window.location.search).get('plan') || ''; - // 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(() => { - if (planDetailsProp) { - setPlanDetails(planDetailsProp); - setPlanLoading(!!planLoadingProp); - setPlanError(''); - return; - } - - const fetchPlan = async () => { - if (!planSlug) return; - setPlanLoading(true); - setPlanError(''); - try { - const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api'; - const res = await fetch(`${API_BASE_URL}/v1/auth/plans/?slug=${planSlug}`); - const data = await res.json(); - const plan = data?.results?.[0]; - if (!plan) { - setPlanError('Plan not found or inactive.'); - } else { - const features = Array.isArray(plan.features) - ? plan.features.map((f: string) => f.charAt(0).toUpperCase() + f.slice(1)) - : []; - setPlanDetails({ ...plan, features }); - } - } catch (e: any) { - setPlanError('Unable to load plan details right now.'); - } finally { - setPlanLoading(false); - } - }; - fetchPlan(); - }, [planSlug, planDetailsProp, planLoadingProp]); - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData((prev) => ({ ...prev, [name]: value })); - }; - - const handleBillingChange = (field: keyof BillingFormData, value: string) => { - setBillingData((prev) => ({ ...prev, [field]: value })); - }; - - const handleNextStep = () => { - setError(''); - - if (currentStep === 1) { - // Validate step 1 - 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; - } - - // Auto-fill billing email if not set - if (isPaidPlan && !billingData.billing_email) { - setBillingData((prev) => ({ ...prev, billing_email: formData.email })); - } - - setCurrentStep(2); - } else if (currentStep === 2) { - // Validate step 2 (billing) - if (!billingData.billing_email || !billingData.billing_address_line1 || - !billingData.billing_city || !billingData.billing_state || - !billingData.billing_postal_code || !billingData.billing_country) { - setError('Please fill in all required billing fields'); - return; - } - setCurrentStep(3); - } - }; - - const handlePrevStep = () => { - setError(''); - setCurrentStep((prev) => Math.max(1, prev - 1)); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - - // Final validation - 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; - } - - // Validate billing for paid plans - if (isPaidPlan) { - if (!billingData.billing_email || !billingData.billing_address_line1 || - !billingData.billing_city || !billingData.billing_state || - !billingData.billing_postal_code || !billingData.billing_country) { - setError('Please fill in all required billing fields'); - setCurrentStep(2); - return; - } - if (!selectedPaymentMethod) { - setError('Please select a payment method'); - setCurrentStep(3); - return; - } - } - - try { - const username = formData.username || formData.email.split('@')[0]; - - const registerPayload: any = { - email: formData.email, - password: formData.password, - username: username, - first_name: formData.firstName, - last_name: formData.lastName, - account_name: formData.accountName, - plan_slug: planSlug || undefined, - }; - - // Add billing fields for paid plans - if (isPaidPlan) { - registerPayload.billing_email = billingData.billing_email; - registerPayload.billing_address_line1 = billingData.billing_address_line1; - registerPayload.billing_address_line2 = billingData.billing_address_line2 || undefined; - registerPayload.billing_city = billingData.billing_city; - registerPayload.billing_state = billingData.billing_state; - registerPayload.billing_postal_code = billingData.billing_postal_code; - registerPayload.billing_country = billingData.billing_country; - registerPayload.tax_id = billingData.tax_id || undefined; - registerPayload.payment_method = selectedPaymentMethod?.payment_method; - } - - const user = await register(registerPayload) as any; - - const status = user?.account?.status; - if (status === 'pending_payment') { - navigate('/account/plans', { replace: true }); - } else { - navigate('/sites', { replace: true }); - } - } catch (err: any) { - setError(err.message || 'Registration failed. Please try again.'); - } - }; - - // Render step indicator - const renderStepIndicator = () => { - if (!isPaidPlan) return null; - - return ( -
-
- {[1, 2, 3].map((step) => ( -
-
-
- {step < currentStep ? : step} -
-
-
- {step === 1 ? 'Account' : step === 2 ? 'Billing' : 'Payment'} -
-
-
- {step < 3 && ( -
- )} -
- ))} -
-
- ); - }; - - return ( -
-
- - - Back to dashboard - -
- -
-
-
-

- {isPaidPlan ? `Sign Up for ${planDetails?.name || 'Paid'} Plan` : 'Start Your Free Trial'} -

-

- {isPaidPlan - ? `Complete the ${totalSteps}-step process to activate your subscription.` - : 'No credit card required. Start creating content today.'} -

-
- - {renderStepIndicator()} - - {error && ( -
- {error} -
- )} - -
- {/* Step 1: Basic Info */} - {currentStep === 1 && ( -
-
-
- - -
-
- - -
-
- -
- - -
- -
- - -
- -
- -
- - 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 -

-
- - {isPaidPlan ? ( - - ) : ( - - )} -
- )} - - {/* Step 2: Billing Info */} - {currentStep === 2 && isPaidPlan && ( -
- -
- - -
-
- )} - - {/* Step 3: Payment Method */} - {currentStep === 3 && isPaidPlan && ( -
- -
- - -
-
- )} -
- -
-

- Already have an account?{' '} - - Sign In - -

-
-
-
-
- ); -} diff --git a/frontend/src/components/auth/SignUpFormSimplified.tsx b/frontend/src/components/auth/SignUpFormSimplified.tsx deleted file mode 100644 index 6eb4ea54..00000000 --- a/frontend/src/components/auth/SignUpFormSimplified.tsx +++ /dev/null @@ -1,437 +0,0 @@ -/** - * Simplified Single-Page Signup Form - * Shows all fields on one page - no multi-step wizard - * For paid plans: registration + payment selection on same page - */ - -import { useState, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { ChevronLeftIcon, EyeCloseIcon, EyeIcon, CreditCardIcon, Building2Icon, WalletIcon, CheckIcon, Loader2Icon } from '../../icons'; -import Label from '../form/Label'; -import Input from '../form/input/InputField'; -import Checkbox from '../form/input/Checkbox'; -import Button from '../ui/button/Button'; -import SelectDropdown from '../form/SelectDropdown'; -import { useAuthStore } from '../../store/authStore'; - -interface PaymentMethodConfig { - id: number; - payment_method: string; - display_name: string; - instructions: string | null; - country_code: string; - is_enabled: boolean; -} - -interface SignUpFormSimplifiedProps { - planDetails?: any; - planLoading?: boolean; -} - -export default function SignUpFormSimplified({ planDetails: planDetailsProp, planLoading: planLoadingProp }: SignUpFormSimplifiedProps) { - const [showPassword, setShowPassword] = useState(false); - const [isChecked, setIsChecked] = useState(false); - - const [formData, setFormData] = useState({ - firstName: '', - lastName: '', - email: '', - password: '', - accountName: '', - billingCountry: 'US', // Default to US for payment method filtering - }); - - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(''); - const [paymentMethods, setPaymentMethods] = useState([]); - const [paymentMethodsLoading, setPaymentMethodsLoading] = useState(false); - - const [error, setError] = useState(''); - const [planDetails, setPlanDetails] = useState(planDetailsProp || null); - const [planLoading, setPlanLoading] = useState(planLoadingProp || false); - - const navigate = useNavigate(); - const { register, loading } = useAuthStore(); - - const planSlug = new URLSearchParams(window.location.search).get('plan') || ''; - const paidPlans = ['starter', 'growth', 'scale']; - const isPaidPlan = planSlug && paidPlans.includes(planSlug); - - // Load plan details - useEffect(() => { - if (planDetailsProp) { - setPlanDetails(planDetailsProp); - setPlanLoading(!!planLoadingProp); - return; - } - - const fetchPlan = async () => { - if (!planSlug) return; - setPlanLoading(true); - try { - const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api'; - const res = await fetch(`${API_BASE_URL}/v1/auth/plans/?slug=${planSlug}`); - const data = await res.json(); - const plan = data?.results?.[0]; - if (plan) { - setPlanDetails(plan); - } - } catch (e: any) { - console.error('Failed to load plan:', e); - } finally { - setPlanLoading(false); - } - }; - fetchPlan(); - }, [planSlug, planDetailsProp, planLoadingProp]); - - // Load payment methods for paid plans - useEffect(() => { - if (!isPaidPlan) return; - - const loadPaymentMethods = async () => { - setPaymentMethodsLoading(true); - try { - const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api'; - const country = formData.billingCountry || 'US'; - const response = await fetch(`${API_BASE_URL}/v1/billing/admin/payment-methods/?country=${country}`); - - if (!response.ok) { - throw new Error('Failed to load payment methods'); - } - - const data = await response.json(); - - // Handle different response formats - let methodsList: PaymentMethodConfig[] = []; - if (Array.isArray(data)) { - methodsList = data; - } else if (data.success && data.data) { - methodsList = Array.isArray(data.data) ? data.data : data.data.results || []; - } else if (data.results) { - methodsList = data.results; - } - - const enabledMethods = methodsList.filter((m: PaymentMethodConfig) => m.is_enabled); - setPaymentMethods(enabledMethods); - - // Auto-select first method - if (enabledMethods.length > 0) { - setSelectedPaymentMethod(enabledMethods[0].payment_method); - } - } catch (err: any) { - console.error('Failed to load payment methods:', err); - setError('Failed to load payment options. Please refresh the page.'); - } finally { - setPaymentMethodsLoading(false); - } - }; - - loadPaymentMethods(); - }, [isPaidPlan, formData.billingCountry]); - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData((prev) => ({ ...prev, [name]: value })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - - // Validation - 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; - } - - // Validate payment method for paid plans - if (isPaidPlan && !selectedPaymentMethod) { - setError('Please select a payment method'); - return; - } - - try { - const username = formData.email.split('@')[0]; - - const registerPayload: any = { - email: formData.email, - password: formData.password, - username: username, - first_name: formData.firstName, - last_name: formData.lastName, - account_name: formData.accountName, - plan_slug: planSlug || undefined, - }; - - // Add payment method for paid plans - if (isPaidPlan) { - registerPayload.payment_method = selectedPaymentMethod; - // Use email as billing email by default - registerPayload.billing_email = formData.email; - registerPayload.billing_country = formData.billingCountry; - } - - const user = await register(registerPayload) as any; - - // Wait a bit for token to persist - await new Promise(resolve => setTimeout(resolve, 100)); - - const status = user?.account?.status; - if (status === 'pending_payment') { - navigate('/account/plans', { replace: true }); - } else { - navigate('/sites', { replace: true }); - } - } catch (err: any) { - setError(err.message || 'Registration failed. Please try again.'); - } - }; - - const getPaymentIcon = (method: string) => { - switch (method) { - case 'stripe': - return ; - case 'bank_transfer': - return ; - case 'local_wallet': - return ; - default: - return ; - } - }; - - return ( -
-
- - - Back to dashboard - -
- -
-
-
-

- {isPaidPlan ? `Sign Up for ${planDetails?.name || 'Paid'} Plan` : 'Start Your Free Trial'} -

-

- {isPaidPlan - ? 'Complete your registration and select a payment method.' - : 'No credit card required. Start creating content today.'} -

-
- - {error && ( -
- {error} -
- )} - -
- {/* Basic Info */} -
-
- - -
-
- - -
-
- -
- - -
- -
- - -
- -
- -
- - setShowPassword(!showPassword)} - className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2" - > - {showPassword ? ( - - ) : ( - - )} - -
-
- - {/* Payment Method Selection for Paid Plans */} - {isPaidPlan && ( -
- {/* Country Selection */} -
- - setFormData({ ...formData, billingCountry: val })} - /> -

- Payment methods will be filtered by your country -

-
- -
- -

- Select how you'd like to pay for your subscription -

-
- - {paymentMethodsLoading ? ( -
- - Loading payment options... -
- ) : paymentMethods.length === 0 ? ( -
-

No payment methods available. Please contact support.

-
- ) : ( -
- {paymentMethods.map((method) => ( -
setSelectedPaymentMethod(method.payment_method)} - className={` - relative p-4 rounded-lg border-2 cursor-pointer transition-all - ${selectedPaymentMethod === method.payment_method - ? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20' - : 'border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600' - } - `} - > -
-
- {getPaymentIcon(method.payment_method)} -
-
-
-

- {method.display_name} -

- {selectedPaymentMethod === method.payment_method && ( - - )} -
- {method.instructions && ( -

- {method.instructions} -

- )} -
-
-
- ))} -
- )} -
- )} - - {/* Terms and Conditions */} -
- -

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

-
- - -
- -
-

- Already have an account?{' '} - - Sign In - -

-
-
-
-
- ); -} diff --git a/frontend/src/components/auth/SignUpFormUnified.tsx b/frontend/src/components/auth/SignUpFormUnified.tsx index db7c8e8d..b8546f49 100644 --- a/frontend/src/components/auth/SignUpFormUnified.tsx +++ b/frontend/src/components/auth/SignUpFormUnified.tsx @@ -121,9 +121,11 @@ export default function SignUpFormUnified({ } // Try to detect user's country for default selection + // Note: This may fail due to CORS - that's expected and handled gracefully try { const geoResponse = await fetch('https://ipapi.co/country_code/', { signal: AbortSignal.timeout(3000), + mode: 'cors', }); if (geoResponse.ok) { const countryCode = await geoResponse.text(); @@ -131,8 +133,9 @@ export default function SignUpFormUnified({ setFormData(prev => ({ ...prev, billingCountry: countryCode.trim() })); } } - } catch { - // Silently fail - keep default US + } catch (error) { + // Silently fail - CORS or network error, keep default US + // This is expected behavior and not a critical error } } catch (err) { console.error('Failed to load countries:', err); @@ -283,28 +286,24 @@ export default function SignUpFormUnified({ billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-28' }`} > - - +
)} -

- Your country determines available payment methods -

@@ -496,28 +492,24 @@ export default function SignUpFormUnified({ billingPeriod === 'monthly' ? 'translate-x-0' : 'translate-x-32' }`} > - - +