Complete Implemenation of tenancy

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-09 00:11:35 +00:00
parent c54db6c2d9
commit bfbade7624
25 changed files with 4959 additions and 35 deletions

View File

@@ -0,0 +1,446 @@
/**
* 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 } from '../../icons';
import { ChevronRight, Check } 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';
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<BillingFormData>({
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<PaymentMethodConfig | null>(null);
const [error, setError] = useState('');
const [planDetails, setPlanDetails] = useState<any | null>(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') || '';
const paidPlans = ['starter', 'growth', 'scale'];
const isPaidPlan = planSlug && paidPlans.includes(planSlug);
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<HTMLInputElement>) => {
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 (
<div className="mb-8">
<div className="flex items-center justify-between">
{[1, 2, 3].map((step) => (
<div key={step} className="flex items-center flex-1">
<div className="flex items-center">
<div
className={`
flex items-center justify-center w-10 h-10 rounded-full font-semibold
${step === currentStep
? 'bg-brand-500 text-white'
: step < currentStep
? 'bg-green-500 text-white'
: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400'
}
`}
>
{step < currentStep ? <Check className="w-5 h-5" /> : step}
</div>
<div className="ml-3">
<div className={`text-sm font-medium ${step === currentStep ? 'text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400'}`}>
{step === 1 ? 'Account' : step === 2 ? 'Billing' : 'Payment'}
</div>
</div>
</div>
{step < 3 && (
<div className={`flex-1 h-0.5 mx-4 ${step < currentStep ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-700'}`} />
)}
</div>
))}
</div>
</div>
);
};
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">
<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 the ${totalSteps}-step process to activate your subscription.`
: 'No credit card required. 1000 AI credits to get started.'}
</p>
</div>
{renderStepIndicator()}
{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}>
{/* Step 1: Basic Info */}
{currentStep === 1 && (
<div className="space-y-5">
<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>
<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>
{isPaidPlan ? (
<Button type="button" variant="primary" onClick={handleNextStep} className="w-full">
Continue to Billing
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
) : (
<Button type="submit" variant="primary" disabled={loading} className="w-full">
{loading ? 'Creating your account...' : 'Start Free Trial'}
</Button>
)}
</div>
)}
{/* Step 2: Billing Info */}
{currentStep === 2 && isPaidPlan && (
<div className="space-y-5">
<BillingFormStep
formData={billingData}
onChange={handleBillingChange}
error={error}
userEmail={formData.email}
/>
<div className="flex gap-3 pt-4">
<Button type="button" variant="outline" onClick={handlePrevStep} className="flex-1">
Back
</Button>
<Button type="button" variant="primary" onClick={handleNextStep} className="flex-1">
Continue to Payment
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
</div>
</div>
)}
{/* Step 3: Payment Method */}
{currentStep === 3 && isPaidPlan && (
<div className="space-y-5">
<PaymentMethodSelect
countryCode={billingData.billing_country}
selectedMethod={selectedPaymentMethod?.payment_method || null}
onSelectMethod={setSelectedPaymentMethod}
error={error}
/>
<div className="flex gap-3 pt-4">
<Button type="button" variant="outline" onClick={handlePrevStep} className="flex-1">
Back
</Button>
<Button type="submit" variant="primary" disabled={loading} className="flex-1">
{loading ? 'Creating account...' : 'Complete Registration'}
</Button>
</div>
</div>
)}
</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>
);
}