final prcing fixes
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.9 on 2025-12-13 20:31
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('igny8_core_auth', '0015_add_plan_original_price'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='plan',
|
||||||
|
name='annual_discount_percent',
|
||||||
|
field=models.IntegerField(default=15, help_text='Annual subscription discount percentage (default 15%)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -185,10 +185,8 @@ class Plan(models.Model):
|
|||||||
help_text="Original price (before discount) - shows as crossed out price. Leave empty if no discount."
|
help_text="Original price (before discount) - shows as crossed out price. Leave empty if no discount."
|
||||||
)
|
)
|
||||||
billing_cycle = models.CharField(max_length=20, choices=BILLING_CYCLE_CHOICES, default='monthly')
|
billing_cycle = models.CharField(max_length=20, choices=BILLING_CYCLE_CHOICES, default='monthly')
|
||||||
annual_discount_percent = models.DecimalField(
|
annual_discount_percent = models.IntegerField(
|
||||||
max_digits=5,
|
default=15,
|
||||||
decimal_places=2,
|
|
||||||
default=15.00,
|
|
||||||
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
||||||
help_text="Annual subscription discount percentage (default 15%)"
|
help_text="Annual subscription discount percentage (default 15%)"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.2.9 on 2025-12-13 20:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('billing', '0015_planlimitusage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='payment',
|
||||||
|
name='payment_account_status_created_idx',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='billing_email',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='billing_period_end',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='billing_period_start',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='payment',
|
||||||
|
name='transaction_reference',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountpaymentmethod',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('stripe', 'Stripe (Credit/Debit Card)'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer (Manual)'), ('local_wallet', 'Local Wallet (Manual)'), ('manual', 'Manual Payment')], db_index=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='credittransaction',
|
||||||
|
name='reference_id',
|
||||||
|
field=models.CharField(blank=True, help_text='DEPRECATED: Use payment FK. Legacy reference (e.g., payment id, invoice id)', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='paymentmethodconfig',
|
||||||
|
name='payment_method',
|
||||||
|
field=models.CharField(choices=[('stripe', 'Stripe (Credit/Debit Card)'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer (Manual)'), ('local_wallet', 'Local Wallet (Manual)'), ('manual', 'Manual Payment')], max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='paymentmethodconfig',
|
||||||
|
name='webhook_url',
|
||||||
|
field=models.URLField(blank=True, help_text='Webhook URL for payment gateway callbacks'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -251,6 +251,12 @@ export default function SignUpFormUnified({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const extractFeatures = (plan: Plan): string[] => {
|
const extractFeatures = (plan: Plan): string[] => {
|
||||||
|
// Use features from plan's JSON field if available, otherwise build from limits
|
||||||
|
if (plan.features && plan.features.length > 0) {
|
||||||
|
return plan.features;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to building from plan limits
|
||||||
const features: string[] = [];
|
const features: string[] = [];
|
||||||
features.push(`${plan.max_sites} ${plan.max_sites === 1 ? 'Site' : 'Sites'}`);
|
features.push(`${plan.max_sites} ${plan.max_sites === 1 ? 'Site' : 'Sites'}`);
|
||||||
features.push(`${plan.max_users} ${plan.max_users === 1 ? 'User' : 'Users'}`);
|
features.push(`${plan.max_users} ${plan.max_users === 1 ? 'User' : 'Users'}`);
|
||||||
@@ -628,9 +634,9 @@ export default function SignUpFormUnified({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features - 3 rows x 2 columns = 6 features */}
|
{/* Features - All features in 2 columns */}
|
||||||
<div className="grid grid-cols-2 gap-x-3 gap-y-2.5">
|
<div className="grid grid-cols-2 gap-x-3 gap-y-2.5">
|
||||||
{features.slice(0, 6).map((feature, idx) => (
|
{features.map((feature, idx) => (
|
||||||
<div key={idx} className="flex items-start gap-2">
|
<div key={idx} className="flex items-start gap-2">
|
||||||
<CheckCircle className="w-4 h-4 text-green-500 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
<CheckCircle className="w-4 h-4 text-green-500 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300 leading-tight">{feature}</span>
|
<span className="text-sm text-gray-700 dark:text-gray-300 leading-tight">{feature}</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
export interface PricingPlan {
|
export interface PricingPlan {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
slug?: string;
|
||||||
name: string;
|
name: string;
|
||||||
price: string | number; // Current displayed price (will be calculated based on period)
|
price: string | number; // Current displayed price (will be calculated based on period)
|
||||||
monthlyPrice?: string | number; // Base monthly price (used for annual discount calculation)
|
monthlyPrice?: string | number; // Base monthly price (used for annual discount calculation)
|
||||||
@@ -100,40 +101,40 @@ export default function PricingTable({
|
|||||||
)}
|
)}
|
||||||
{showToggle && (
|
{showToggle && (
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<div className="relative inline-flex p-1 bg-gray-200 rounded-full dark:bg-gray-800 shadow-sm">
|
<div className="relative inline-flex items-center justify-center">
|
||||||
<span
|
<div className="relative inline-flex p-1 bg-gray-200 rounded-full dark:bg-gray-800 shadow-sm">
|
||||||
className="absolute top-1 left-1 flex h-11 w-[130px] rounded-full shadow-theme-xs duration-200 ease-linear"
|
<span
|
||||||
style={{
|
className="absolute top-1 left-1 flex h-11 w-[130px] rounded-full shadow-theme-xs duration-200 ease-linear"
|
||||||
background: 'linear-gradient(to bottom right, #0693e3, #0472b8)',
|
style={{
|
||||||
transform: billingPeriod === 'monthly' ? 'translateX(0)' : 'translateX(130px)',
|
background: 'linear-gradient(to bottom right, #0693e3, #0472b8)',
|
||||||
}}
|
transform: billingPeriod === 'monthly' ? 'translateX(0)' : 'translateX(130px)',
|
||||||
></span>
|
}}
|
||||||
<button
|
></span>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setBillingPeriod('monthly')}
|
type="button"
|
||||||
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full ${
|
onClick={() => setBillingPeriod('monthly')}
|
||||||
billingPeriod === 'monthly'
|
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full cursor-pointer ${
|
||||||
? 'text-white'
|
billingPeriod === 'monthly'
|
||||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-white/80 dark:text-gray-400'
|
? 'text-white'
|
||||||
}`}
|
: 'text-gray-500 hover:text-gray-700 dark:hover:text-white/80 dark:text-gray-400'
|
||||||
>
|
}`}
|
||||||
Monthly
|
>
|
||||||
</button>
|
Monthly
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setBillingPeriod('annually')}
|
type="button"
|
||||||
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full ${
|
onClick={() => setBillingPeriod('annually')}
|
||||||
billingPeriod === 'annually'
|
className={`relative z-10 flex h-11 w-[130px] items-center justify-center font-medium transition-all duration-200 rounded-full cursor-pointer ${
|
||||||
? 'text-white'
|
billingPeriod === 'annually'
|
||||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-white/80 dark:text-gray-400'
|
? 'text-white'
|
||||||
}`}
|
: 'text-gray-500 hover:text-gray-700 dark:hover:text-white/80 dark:text-gray-400'
|
||||||
>
|
}`}
|
||||||
Annually
|
>
|
||||||
</button>
|
Annually
|
||||||
</div>
|
</button>
|
||||||
{billingPeriod === 'annually' && (
|
</div>
|
||||||
<div className="flex items-center justify-center mt-3">
|
{billingPeriod === 'annually' && (
|
||||||
<span className="inline-flex items-center gap-1.5 text-green-600 dark:text-green-400 font-semibold bg-green-50 dark:bg-green-900/20 px-3 py-1.5 rounded-full text-sm">
|
<span className="absolute left-[calc(100%+1rem)] whitespace-nowrap inline-flex items-center gap-1.5 text-green-600 dark:text-green-400 font-semibold bg-green-50 dark:bg-green-900/20 px-3 py-1.5 rounded-full text-sm">
|
||||||
<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
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
@@ -142,13 +143,13 @@ export default function PricingTable({
|
|||||||
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"
|
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 {plans[0]?.annualDiscountPercent || 15}% with annual billing
|
Save {Math.round(plans[0]?.annualDiscountPercent || 15)}% with annual billing
|
||||||
</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:gap-6">
|
<div className="flex flex-wrap gap-5 xl:gap-6 justify-center max-w-[1660px] mx-auto">
|
||||||
{plans.map((plan, index) => {
|
{plans.map((plan, index) => {
|
||||||
const isHighlighted = plan.highlighted || false; // Use explicit highlighted prop
|
const isHighlighted = plan.highlighted || false; // Use explicit highlighted prop
|
||||||
const displayPrice = getDisplayPrice(plan);
|
const displayPrice = getDisplayPrice(plan);
|
||||||
@@ -157,7 +158,7 @@ export default function PricingTable({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={plan.id || index}
|
key={plan.id || index}
|
||||||
className={`rounded-2xl border p-6 flex flex-col ${
|
className={`rounded-2xl border p-6 flex flex-col flex-1 min-w-[280px] max-w-[380px] ${
|
||||||
isHighlighted
|
isHighlighted
|
||||||
? 'bg-gray-800 border-gray-800 dark:border-white/10 dark:bg-white/10'
|
? 'bg-gray-800 border-gray-800 dark:border-white/10 dark:bg-white/10'
|
||||||
: 'border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]'
|
: 'border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]'
|
||||||
@@ -235,7 +236,7 @@ export default function PricingTable({
|
|||||||
isHighlighted
|
isHighlighted
|
||||||
? 'bg-brand-500 hover:bg-brand-600 dark:hover:bg-brand-600'
|
? 'bg-brand-500 hover:bg-brand-600 dark:hover:bg-brand-600'
|
||||||
: 'bg-gray-800 hover:bg-brand-500 dark:bg-white/10 dark:hover:bg-brand-600'
|
: 'bg-gray-800 hover:bg-brand-500 dark:bg-white/10 dark:hover:bg-brand-600'
|
||||||
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
} ${plan.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
>
|
>
|
||||||
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')}
|
{plan.buttonText || (plan.price === 0 || plan.monthlyPrice === 0 ? 'Start Free' : 'Choose Plan')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface Plan {
|
|||||||
name: string;
|
name: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
price: number | string;
|
price: number | string;
|
||||||
|
original_price?: number | string;
|
||||||
annual_discount_percent?: number;
|
annual_discount_percent?: number;
|
||||||
is_featured?: boolean;
|
is_featured?: boolean;
|
||||||
max_sites?: number;
|
max_sites?: number;
|
||||||
@@ -69,9 +70,11 @@ const convertToPricingPlan = (plan: Plan): PricingPlan => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: plan.id,
|
id: plan.id,
|
||||||
|
slug: plan.slug,
|
||||||
name: plan.name,
|
name: plan.name,
|
||||||
monthlyPrice: monthlyPrice,
|
monthlyPrice: monthlyPrice,
|
||||||
price: monthlyPrice,
|
price: monthlyPrice,
|
||||||
|
originalPrice: plan.original_price ? (typeof plan.original_price === 'number' ? plan.original_price : parseFloat(String(plan.original_price))) : undefined,
|
||||||
annualDiscountPercent: plan.annual_discount_percent || 15,
|
annualDiscountPercent: plan.annual_discount_percent || 15,
|
||||||
period: '/month',
|
period: '/month',
|
||||||
description: description,
|
description: description,
|
||||||
@@ -206,7 +209,7 @@ const Pricing: React.FC = () => {
|
|||||||
<style>{`
|
<style>{`
|
||||||
/* Pricing Table Component Styles - Embedded for Marketing Site */
|
/* Pricing Table Component Styles - Embedded for Marketing Site */
|
||||||
.pricing-table-wrapper {
|
.pricing-table-wrapper {
|
||||||
max-width: 1560px;
|
max-width: 1660px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,10 +381,9 @@ const Pricing: React.FC = () => {
|
|||||||
<div className="pricing-table-wrapper">
|
<div className="pricing-table-wrapper">
|
||||||
<PricingTable1
|
<PricingTable1
|
||||||
plans={plans.map(convertToPricingPlan)}
|
plans={plans.map(convertToPricingPlan)}
|
||||||
title="Choose your plan"
|
|
||||||
showToggle={true}
|
showToggle={true}
|
||||||
onPlanSelect={(plan) => {
|
onPlanSelect={(plan) => {
|
||||||
window.location.href = `/signup?plan=${plan.id}`;
|
window.location.href = `https://app.igny8.com/signup?plan=${plan.slug || plan.id}`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const convertToPricingPlan = (plan: Plan): PricingPlan => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: plan.id,
|
id: plan.id,
|
||||||
|
slug: plan.slug,
|
||||||
name: plan.name,
|
name: plan.name,
|
||||||
monthlyPrice: monthlyPrice,
|
monthlyPrice: monthlyPrice,
|
||||||
price: monthlyPrice,
|
price: monthlyPrice,
|
||||||
|
|||||||
Reference in New Issue
Block a user