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') == '':
|
if 'plan_id' in attrs and attrs.get('plan_id') == '':
|
||||||
attrs['plan_id'] = None
|
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')
|
plan_slug = attrs.get('plan_slug')
|
||||||
paid_plans = ['starter', 'growth', 'scale']
|
if plan_slug:
|
||||||
if plan_slug and plan_slug in paid_plans:
|
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
|
# Require billing_country for paid plans
|
||||||
if not attrs.get('billing_country'):
|
if not attrs.get('billing_country'):
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
@@ -348,27 +356,36 @@ class RegisterSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
plan_slug = validated_data.get('plan_slug')
|
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:
|
try:
|
||||||
plan = Plan.objects.get(slug=plan_slug, is_active=True)
|
plan = Plan.objects.get(slug=plan_slug, is_active=True)
|
||||||
except Plan.DoesNotExist:
|
except Plan.DoesNotExist:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
"plan": f"Plan '{plan_slug}' not available. Please contact support."
|
"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'
|
account_status = 'pending_payment'
|
||||||
initial_credits = 0
|
initial_credits = 0
|
||||||
billing_period_start = timezone.now()
|
billing_period_start = timezone.now()
|
||||||
# simple monthly cycle; if annual needed, extend here
|
# simple monthly cycle; if annual needed, extend here
|
||||||
billing_period_end = billing_period_start + timedelta(days=30)
|
billing_period_end = billing_period_start + timedelta(days=30)
|
||||||
else:
|
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'
|
account_status = 'trial'
|
||||||
initial_credits = plan.get_effective_credits_per_month()
|
initial_credits = plan.get_effective_credits_per_month()
|
||||||
billing_period_start = None
|
billing_period_start = None
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ export default function SignUpFormEnhanced({ planDetails: planDetailsProp, planL
|
|||||||
const { register, loading } = useAuthStore();
|
const { register, loading } = useAuthStore();
|
||||||
|
|
||||||
const planSlug = new URLSearchParams(window.location.search).get('plan') || '';
|
const planSlug = new URLSearchParams(window.location.search).get('plan') || '';
|
||||||
const paidPlans = ['starter', 'growth', 'scale'];
|
// Determine if plan is paid based on price, not hardcoded slug
|
||||||
const isPaidPlan = planSlug && paidPlans.includes(planSlug);
|
const isPaidPlan = planDetails && parseFloat(String(planDetails.price || 0)) > 0;
|
||||||
const totalSteps = isPaidPlan ? 3 : 1;
|
const totalSteps = isPaidPlan ? 3 : 1;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export default function SignUpFormUnified({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('monthly')}
|
onClick={() => setBillingPeriod('monthly')}
|
||||||
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
|
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
|
Monthly
|
||||||
@@ -300,7 +300,7 @@ export default function SignUpFormUnified({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('annually')}
|
onClick={() => setBillingPeriod('annually')}
|
||||||
className={`relative flex h-9 w-28 items-center justify-center text-sm font-semibold transition-all duration-200 rounded-md ${
|
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
|
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">
|
<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" />
|
<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>
|
</svg>
|
||||||
Save {annualDiscountPercent}%
|
Save up to {annualDiscountPercent}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto no-scrollbar flex items-center">
|
<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">
|
<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>
|
<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">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
@@ -502,7 +502,7 @@ export default function SignUpFormUnified({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('monthly')}
|
onClick={() => setBillingPeriod('monthly')}
|
||||||
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
|
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
|
Monthly
|
||||||
@@ -513,7 +513,7 @@ export default function SignUpFormUnified({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setBillingPeriod('annually')}
|
onClick={() => setBillingPeriod('annually')}
|
||||||
className={`relative flex h-11 w-32 items-center justify-center text-base font-semibold transition-all duration-200 rounded-lg ${
|
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
|
Annually
|
||||||
@@ -525,12 +525,12 @@ export default function SignUpFormUnified({
|
|||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
Save {annualDiscountPercent}%
|
Save up to {annualDiscountPercent}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plan Cards - Single column, stacked vertically */}
|
{/* 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) => {
|
{plans.map((plan) => {
|
||||||
const displayPrice = getDisplayPrice(plan);
|
const displayPrice = getDisplayPrice(plan);
|
||||||
const features = extractFeatures(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>
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Status</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Badge variant="light" color={(balance as any)?.subscription_status === 'active' ? 'success' : 'secondary'} className="text-base font-semibold">
|
<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 || 'No subscription'}
|
{(balance as any)?.subscription_status === 'trial' ? 'Active (Trial)' : ((balance as any)?.subscription_status || 'No subscription')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Subscription status</p>
|
<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 && (
|
{subscription?.status && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="soft"
|
variant="soft"
|
||||||
color={subscription.status === 'active' ? 'success' : 'warning'}
|
color={(subscription.status === 'active' || subscription.status === 'trial') ? 'success' : 'warning'}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{subscription.status}
|
{subscription.status === 'trial' ? 'Active (Trial)' : subscription.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import PageMeta from "../../components/common/PageMeta";
|
import PageMeta from "../../components/common/PageMeta";
|
||||||
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
|
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
|
||||||
|
import GridShape from "../../components/common/GridShape";
|
||||||
|
|
||||||
interface Plan {
|
interface Plan {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -89,25 +90,14 @@ export default function SignUp() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Right Side - Pricing Plans */}
|
{/* 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">
|
<div className="hidden lg:flex lg:w-1/2 bg-brand-950 dark:bg-white/5 items-center justify-center relative">
|
||||||
{/* GridShape Background */}
|
{/* GridShape Background - Same as signin page */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 z-0">
|
||||||
<div className="relative flex items-center justify-center z-1 w-full h-full">
|
<GridShape />
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Logo - Top Right - Smaller */}
|
{/* 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
|
<img
|
||||||
src="/images/logo/IGNY8_DARK_LOGO.png"
|
src="/images/logo/IGNY8_DARK_LOGO.png"
|
||||||
alt="IGNY8"
|
alt="IGNY8"
|
||||||
@@ -115,7 +105,7 @@ export default function SignUp() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</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 */}
|
{/* Pricing Plans Component Will Load Here */}
|
||||||
<div id="signup-pricing-plans" className="w-full">
|
<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
|
// FIX: hasActivePlan should check account status, not just plan existence
|
||||||
const accountStatus = user?.account?.status || '';
|
const accountStatus = user?.account?.status || '';
|
||||||
const hasPendingInvoice = invoices.some((inv) => inv.status === 'pending');
|
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
|
&& effectivePlanId
|
||||||
&& currentPlan?.slug !== 'free'
|
|
||||||
&& !hasPendingInvoice;
|
&& !hasPendingInvoice;
|
||||||
|
|
||||||
const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');
|
const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');
|
||||||
|
|||||||
Reference in New Issue
Block a user