444 lines
16 KiB
TypeScript
444 lines
16 KiB
TypeScript
/**
|
|
* 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<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') || '';
|
|
// 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<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-success-500 text-white'
|
|
: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400'
|
|
}
|
|
`}
|
|
>
|
|
{step < currentStep ? <CheckIcon 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-success-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. Start creating content today.'}
|
|
</p>
|
|
</div>
|
|
|
|
{renderStepIndicator()}
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 text-sm text-error-600 bg-error-50 border border-error-200 rounded-lg dark:bg-error-900/20 dark:text-error-400 dark:border-error-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" endIcon={<ChevronRightIcon className="w-4 h-4" />}>
|
|
Continue to Billing
|
|
</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" endIcon={<ChevronRightIcon className="w-4 h-4" />}>
|
|
Continue to Payment
|
|
</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>
|
|
);
|
|
}
|