free and trial plans fixes and styling of sigini and signup forms
This commit is contained in:
@@ -320,10 +320,18 @@ class RegisterSerializer(serializers.Serializer):
|
||||
if 'plan_id' in attrs and attrs.get('plan_id') == '':
|
||||
attrs['plan_id'] = None
|
||||
|
||||
# Validate billing fields for paid plans
|
||||
# Validate billing fields for paid plans (check by price, not hardcoded slugs)
|
||||
plan_slug = attrs.get('plan_slug')
|
||||
paid_plans = ['starter', 'growth', 'scale']
|
||||
if plan_slug and plan_slug in paid_plans:
|
||||
if plan_slug:
|
||||
try:
|
||||
plan = Plan.objects.get(slug=plan_slug, is_active=True)
|
||||
is_paid_plan = plan.price > 0
|
||||
except Plan.DoesNotExist:
|
||||
raise serializers.ValidationError({"plan": f"Plan '{plan_slug}' not found."})
|
||||
else:
|
||||
is_paid_plan = False
|
||||
|
||||
if is_paid_plan:
|
||||
# Require billing_country for paid plans
|
||||
if not attrs.get('billing_country'):
|
||||
raise serializers.ValidationError({
|
||||
@@ -348,27 +356,36 @@ class RegisterSerializer(serializers.Serializer):
|
||||
|
||||
with transaction.atomic():
|
||||
plan_slug = validated_data.get('plan_slug')
|
||||
paid_plans = ['starter', 'growth', 'scale']
|
||||
|
||||
if plan_slug and plan_slug in paid_plans:
|
||||
# Fetch plan and determine paid/free by price, not hardcoded slugs
|
||||
if plan_slug:
|
||||
try:
|
||||
plan = Plan.objects.get(slug=plan_slug, is_active=True)
|
||||
except Plan.DoesNotExist:
|
||||
raise serializers.ValidationError({
|
||||
"plan": f"Plan '{plan_slug}' not available. Please contact support."
|
||||
})
|
||||
else:
|
||||
# No plan_slug provided, get first free plan (price=0)
|
||||
try:
|
||||
plan = Plan.objects.filter(is_active=True, price=0).first()
|
||||
if not plan:
|
||||
raise Plan.DoesNotExist
|
||||
except Plan.DoesNotExist:
|
||||
raise serializers.ValidationError({
|
||||
"plan": "No free plan available. Please contact support."
|
||||
})
|
||||
|
||||
# Determine account status based on plan price
|
||||
is_paid_plan = plan.price > 0
|
||||
|
||||
if is_paid_plan:
|
||||
account_status = 'pending_payment'
|
||||
initial_credits = 0
|
||||
billing_period_start = timezone.now()
|
||||
# simple monthly cycle; if annual needed, extend here
|
||||
billing_period_end = billing_period_start + timedelta(days=30)
|
||||
else:
|
||||
try:
|
||||
plan = Plan.objects.get(slug='free', is_active=True)
|
||||
except Plan.DoesNotExist:
|
||||
raise serializers.ValidationError({
|
||||
"plan": "Free plan not configured. Please contact support."
|
||||
})
|
||||
account_status = 'trial'
|
||||
initial_credits = plan.get_effective_credits_per_month()
|
||||
billing_period_start = None
|
||||
|
||||
@@ -60,8 +60,8 @@ export default function SignUpFormEnhanced({ planDetails: planDetailsProp, planL
|
||||
const { register, loading } = useAuthStore();
|
||||
|
||||
const planSlug = new URLSearchParams(window.location.search).get('plan') || '';
|
||||
const paidPlans = ['starter', 'growth', 'scale'];
|
||||
const isPaidPlan = planSlug && paidPlans.includes(planSlug);
|
||||
// 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(() => {
|
||||
|
||||
@@ -289,7 +289,7 @@ export default function SignUpFormUnified({
|
||||
size="sm"
|
||||
onClick={() => setBillingPeriod('monthly')}
|
||||
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
|
||||
billingPeriod === 'monthly' ? 'text-white hover:text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||
billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
@@ -300,7 +300,7 @@ export default function SignUpFormUnified({
|
||||
size="sm"
|
||||
onClick={() => setBillingPeriod('annually')}
|
||||
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
|
||||
billingPeriod === 'annually' ? 'text-white hover:text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||
billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
Annually
|
||||
@@ -312,13 +312,13 @@ export default function SignUpFormUnified({
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Save {annualDiscountPercent}%
|
||||
Save up to {annualDiscountPercent}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto no-scrollbar flex items-center">
|
||||
<div className="w-full mx-auto p-6 sm:p-8 max-w-2xl">
|
||||
<div className="w-full mx-auto p-6 sm:p-8 max-w-[572px]">
|
||||
<div className="mb-6">
|
||||
<h1 className="mb-2 font-semibold text-gray-800 dark:text-white text-2xl">Sign Up for {selectedPlan?.name || 'IGNY8'}</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
@@ -502,7 +502,7 @@ export default function SignUpFormUnified({
|
||||
size="sm"
|
||||
onClick={() => setBillingPeriod('monthly')}
|
||||
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
|
||||
billingPeriod === 'monthly' ? 'text-white hover:text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||
billingPeriod === 'monthly' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
@@ -513,7 +513,7 @@ export default function SignUpFormUnified({
|
||||
size="sm"
|
||||
onClick={() => setBillingPeriod('annually')}
|
||||
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
|
||||
billingPeriod === 'annually' ? 'text-white hover:text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||
billingPeriod === 'annually' ? 'text-white' : 'text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
Annually
|
||||
@@ -525,12 +525,12 @@ export default function SignUpFormUnified({
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Save {annualDiscountPercent}%
|
||||
Save up to {annualDiscountPercent}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Plan Cards - Single column, stacked vertically */}
|
||||
<div className="grid gap-4 grid-cols-1 w-full max-w-[840px] mx-auto">
|
||||
<div className="grid gap-4 grid-cols-1 w-full max-w-[640px] mx-auto">
|
||||
{plans.map((plan) => {
|
||||
const displayPrice = getDisplayPrice(plan);
|
||||
const features = extractFeatures(plan);
|
||||
|
||||
@@ -78,8 +78,8 @@ export default function BillingBalancePanel() {
|
||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Status</h3>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Badge variant="light" color={(balance as any)?.subscription_status === 'active' ? 'success' : 'secondary'} className="text-base font-semibold">
|
||||
{(balance as any)?.subscription_status || 'No subscription'}
|
||||
<Badge variant="light" color={((balance as any)?.subscription_status === 'active' || (balance as any)?.subscription_status === 'trial') ? 'success' : 'secondary'} className="text-base font-semibold">
|
||||
{(balance as any)?.subscription_status === 'trial' ? 'Active (Trial)' : ((balance as any)?.subscription_status || 'No subscription')}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Subscription status</p>
|
||||
|
||||
@@ -117,10 +117,10 @@ export default function AccountInfoWidget({
|
||||
{subscription?.status && (
|
||||
<Badge
|
||||
variant="soft"
|
||||
color={subscription.status === 'active' ? 'success' : 'warning'}
|
||||
color={(subscription.status === 'active' || subscription.status === 'trial') ? 'success' : 'warning'}
|
||||
size="sm"
|
||||
>
|
||||
{subscription.status}
|
||||
{subscription.status === 'trial' ? 'Active (Trial)' : subscription.status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
|
||||
import GridShape from "../../components/common/GridShape";
|
||||
|
||||
interface Plan {
|
||||
id: number;
|
||||
@@ -89,25 +90,14 @@ export default function SignUp() {
|
||||
/>
|
||||
|
||||
{/* Right Side - Pricing Plans */}
|
||||
<div className="hidden lg:flex lg:w-1/2 bg-brand-950 dark:bg-white/5 p-8 xl:p-12 items-center justify-center relative">
|
||||
{/* GridShape Background */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="relative flex items-center justify-center z-1 w-full h-full">
|
||||
<div className="absolute inset-0">
|
||||
<svg className="w-full h-full opacity-40" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100" height="100" fill="url(#grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:flex lg:w-1/2 bg-brand-950 dark:bg-white/5 items-center justify-center relative">
|
||||
{/* GridShape Background - Same as signin page */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<GridShape />
|
||||
</div>
|
||||
|
||||
{/* Logo - Top Right - Smaller */}
|
||||
<Link to="/" className="absolute top-6 right-6 z-10">
|
||||
<Link to="/" className="absolute top-6 right-6 z-20">
|
||||
<img
|
||||
src="/images/logo/IGNY8_DARK_LOGO.png"
|
||||
alt="IGNY8"
|
||||
@@ -115,7 +105,7 @@ export default function SignUp() {
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="w-full max-w-[840px] relative z-10">
|
||||
<div className="w-full max-w-[640px] relative z-10">
|
||||
|
||||
{/* Pricing Plans Component Will Load Here */}
|
||||
<div id="signup-pricing-plans" className="w-full">
|
||||
|
||||
@@ -643,9 +643,9 @@ export default function PlansAndBillingPage() {
|
||||
// FIX: hasActivePlan should check account status, not just plan existence
|
||||
const accountStatus = user?.account?.status || '';
|
||||
const hasPendingInvoice = invoices.some((inv) => inv.status === 'pending');
|
||||
const hasActivePlan = accountStatus === 'active'
|
||||
// Accept both 'active' and 'trial' status (free plans get 'trial' status)
|
||||
const hasActivePlan = (accountStatus === 'active' || accountStatus === 'trial')
|
||||
&& effectivePlanId
|
||||
&& currentPlan?.slug !== 'free'
|
||||
&& !hasPendingInvoice;
|
||||
|
||||
const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');
|
||||
|
||||
Reference in New Issue
Block a user