fixes fixes fixes tenaancy
This commit is contained in:
409
frontend/src/components/auth/SignUpFormSimplified.tsx
Normal file
409
frontend/src/components/auth/SignUpFormSimplified.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* 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 } from '../../icons';
|
||||
import { CreditCard, Building2, Wallet, Check, Loader2 } from 'lucide-react';
|
||||
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';
|
||||
|
||||
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: '',
|
||||
});
|
||||
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>('');
|
||||
const [paymentMethods, setPaymentMethods] = useState<PaymentMethodConfig[]>([]);
|
||||
const [paymentMethodsLoading, setPaymentMethodsLoading] = useState(false);
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const [planDetails, setPlanDetails] = useState<any | null>(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 response = await fetch(`${API_BASE_URL}/v1/billing/admin/payment-methods/`);
|
||||
|
||||
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]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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;
|
||||
}
|
||||
|
||||
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 <CreditCard className="w-5 h-5" />;
|
||||
case 'bank_transfer':
|
||||
return <Building2 className="w-5 h-5" />;
|
||||
case 'local_wallet':
|
||||
return <Wallet className="w-5 h-5" />;
|
||||
default:
|
||||
return <CreditCard className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 w-full overflow-y-auto lg:w-1/2 no-scrollbar">
|
||||
<div className="w-full max-w-md mx-auto mb-5 sm:pt-10">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
<ChevronLeftIcon className="size-5" />
|
||||
Back to dashboard
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto pb-10">
|
||||
<div>
|
||||
<div className="mb-5 sm:mb-8">
|
||||
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
|
||||
{isPaidPlan ? `Sign Up for ${planDetails?.name || 'Paid'} Plan` : 'Start Your Free Trial'}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{isPaidPlan
|
||||
? 'Complete your registration and select a payment method.'
|
||||
: 'No credit card required. 1000 AI credits to get started.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-lg dark:bg-red-900/20 dark:text-red-400 dark:border-red-800">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Basic Info */}
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label>
|
||||
First Name<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter your first name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Last Name<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter your last name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
Email<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Account Name (optional)</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="accountName"
|
||||
value={formData.accountName}
|
||||
onChange={handleChange}
|
||||
placeholder="Workspace / Company name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
Password<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Enter your password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeIcon className="fill-gray-500 dark:fill-gray-400 size-5" />
|
||||
) : (
|
||||
<EyeCloseIcon className="fill-gray-500 dark:fill-gray-400 size-5" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Method Selection for Paid Plans */}
|
||||
{isPaidPlan && (
|
||||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="mb-3">
|
||||
<Label>
|
||||
Payment Method<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Select how you'd like to pay for your subscription
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{paymentMethodsLoading ? (
|
||||
<div className="flex items-center justify-center p-6 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-brand-500 mr-2" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Loading payment options...</span>
|
||||
</div>
|
||||
) : paymentMethods.length === 0 ? (
|
||||
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-800 dark:bg-amber-900/20 dark:border-amber-800 dark:text-amber-200">
|
||||
<p className="text-sm">No payment methods available. Please contact support.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{paymentMethods.map((method) => (
|
||||
<div
|
||||
key={method.id}
|
||||
onClick={() => 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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`
|
||||
flex items-center justify-center w-10 h-10 rounded-lg
|
||||
${selectedPaymentMethod === method.payment_method
|
||||
? 'bg-brand-500 text-white'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
|
||||
}
|
||||
`}>
|
||||
{getPaymentIcon(method.payment_method)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{method.display_name}
|
||||
</h4>
|
||||
{selectedPaymentMethod === method.payment_method && (
|
||||
<Check className="w-5 h-5 text-brand-500" />
|
||||
)}
|
||||
</div>
|
||||
{method.instructions && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 whitespace-pre-line">
|
||||
{method.instructions}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terms and Conditions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox className="w-5 h-5" checked={isChecked} onChange={setIsChecked} />
|
||||
<p className="inline-block font-normal text-gray-500 dark:text-gray-400">
|
||||
By creating an account means you agree to the{' '}
|
||||
<span className="text-gray-800 dark:text-white/90">Terms and Conditions,</span> and
|
||||
our <span className="text-gray-800 dark:text-white">Privacy Policy</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button type="submit" variant="primary" disabled={loading} className="w-full">
|
||||
{loading ? 'Creating your account...' : isPaidPlan ? 'Create Account & Continue to Payment' : 'Start Free Trial'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-5">
|
||||
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
|
||||
Already have an account?{' '}
|
||||
<Link to="/signin" className="text-brand-500 hover:text-brand-600 dark:text-brand-400">
|
||||
Sign In
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ interface PaymentConfirmationModalProps {
|
||||
invoice: {
|
||||
id: number;
|
||||
invoice_number: string;
|
||||
total_amount: string;
|
||||
total_amount: string; // Backend returns 'total_amount' in API response
|
||||
currency?: string;
|
||||
};
|
||||
paymentMethod: {
|
||||
|
||||
@@ -15,7 +15,7 @@ import PaymentConfirmationModal from './PaymentConfirmationModal';
|
||||
interface Invoice {
|
||||
id: number;
|
||||
invoice_number: string;
|
||||
total_amount: string;
|
||||
total_amount: string; // Backend returns 'total_amount' in serialized response
|
||||
currency: string;
|
||||
status: string;
|
||||
due_date?: string;
|
||||
@@ -75,8 +75,9 @@ export default function PendingPaymentBanner({ className = '' }: PendingPaymentB
|
||||
});
|
||||
|
||||
const pmData = await pmResponse.json();
|
||||
if (pmResponse.ok && pmData.success && pmData.results?.length > 0) {
|
||||
setPaymentMethod(pmData.results[0]);
|
||||
// API returns array directly from DRF Response
|
||||
if (pmResponse.ok && Array.isArray(pmData) && pmData.length > 0) {
|
||||
setPaymentMethod(pmData[0]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import AuthLayout from "./AuthPageLayout";
|
||||
import SignUpFormEnhanced from "../../components/auth/SignUpFormEnhanced";
|
||||
import SignUpFormSimplified from "../../components/auth/SignUpFormSimplified";
|
||||
|
||||
export default function SignUp() {
|
||||
const planSlug = useMemo(() => {
|
||||
@@ -40,11 +40,11 @@ export default function SignUp() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js SignUp Dashboard | TailAdmin - Next.js Admin Dashboard Template"
|
||||
description="This is React.js SignUp Tables Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
title="Sign Up - IGNY8"
|
||||
description="Create your IGNY8 account and start building topical authority with AI-powered content"
|
||||
/>
|
||||
<AuthLayout plan={planDetails}>
|
||||
<SignUpFormEnhanced planDetails={planDetails} planLoading={planLoading} />
|
||||
<SignUpFormSimplified planDetails={planDetails} planLoading={planLoading} />
|
||||
</AuthLayout>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -118,6 +118,14 @@ export const useAuthStore = create<AuthState>()(
|
||||
version: 0
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(authState));
|
||||
|
||||
// CRITICAL: Also set tokens as separate items for API interceptor
|
||||
if (newToken) {
|
||||
localStorage.setItem('access_token', newToken);
|
||||
}
|
||||
if (newRefreshToken) {
|
||||
localStorage.setItem('refresh_token', newRefreshToken);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to persist auth state to localStorage:', e);
|
||||
}
|
||||
@@ -229,6 +237,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
const newRefreshToken = tokens.refresh || responseData.refresh || data.refresh || null;
|
||||
|
||||
// CRITICAL: Set auth state AND immediately persist to localStorage
|
||||
// This prevents race conditions where navigation happens before persist
|
||||
set({
|
||||
user: userData,
|
||||
token: newToken,
|
||||
@@ -250,10 +259,20 @@ export const useAuthStore = create<AuthState>()(
|
||||
version: 0
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(authState));
|
||||
|
||||
// CRITICAL: Also set tokens as separate items for API interceptor
|
||||
// This ensures fetchAPI can access tokens immediately
|
||||
if (newToken) {
|
||||
localStorage.setItem('access_token', newToken);
|
||||
}
|
||||
if (newRefreshToken) {
|
||||
localStorage.setItem('refresh_token', newRefreshToken);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to persist auth state to localStorage:', e);
|
||||
}
|
||||
|
||||
// Return user data for success handling
|
||||
return userData;
|
||||
} catch (error: any) {
|
||||
// ALWAYS reset loading on error - critical to prevent stuck state
|
||||
@@ -261,7 +280,6 @@ export const useAuthStore = create<AuthState>()(
|
||||
throw new Error(error.message || 'Registration failed');
|
||||
} finally {
|
||||
// Extra safety: ensure loading is ALWAYS false after register attempt completes
|
||||
// This handles edge cases like network timeouts, browser crashes, etc.
|
||||
const current = get();
|
||||
if (current.loading) {
|
||||
set({ loading: false });
|
||||
|
||||
Reference in New Issue
Block a user