feat(billing): add missing payment methods and configurations
- Added migration to include global payment method configurations for Stripe and PayPal (both disabled). - Ensured existing payment methods like bank transfer and manual payment are correctly configured. - Added database constraints and indexes for improved data integrity in billing models. - Introduced foreign key relationship between CreditTransaction and Payment models. - Added webhook configuration fields to PaymentMethodConfig for future payment gateway integrations. - Updated SignUpFormUnified component to handle payment method selection based on user country and plan. - Implemented PaymentHistory component to display user's payment history with status indicators.
This commit is contained in:
@@ -1,7 +1,30 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import AuthLayout from "./AuthPageLayout";
|
||||
import SignUpFormSimplified from "../../components/auth/SignUpFormSimplified";
|
||||
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
|
||||
|
||||
interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
price: string | number;
|
||||
billing_cycle: string;
|
||||
is_active: boolean;
|
||||
max_users: number;
|
||||
max_sites: number;
|
||||
max_keywords: number;
|
||||
max_clusters: number;
|
||||
max_content_ideas: number;
|
||||
monthly_word_count_limit: number;
|
||||
monthly_ai_credit_limit: number;
|
||||
monthly_image_count: number;
|
||||
daily_content_tasks: number;
|
||||
daily_ai_request_limit: number;
|
||||
daily_image_generation_limit: number;
|
||||
included_credits: number;
|
||||
image_model_choices: string[];
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export default function SignUp() {
|
||||
const planSlug = useMemo(() => {
|
||||
@@ -9,29 +32,45 @@ export default function SignUp() {
|
||||
return params.get("plan") || "";
|
||||
}, []);
|
||||
|
||||
const [planDetails, setPlanDetails] = useState<any | null>(null);
|
||||
const [planLoading, setPlanLoading] = useState(false);
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [plansLoading, setPlansLoading] = useState(true);
|
||||
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
if (!planSlug) return;
|
||||
setPlanLoading(true);
|
||||
setPlansLoading(true);
|
||||
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/`);
|
||||
const data = await res.json();
|
||||
const plans = data?.results || [];
|
||||
const plan = plans.find((p: any) => p.slug === planSlug);
|
||||
if (plan) {
|
||||
const features = Array.isArray(plan.features)
|
||||
? plan.features.map((f: string) => f.charAt(0).toUpperCase() + f.slice(1))
|
||||
: [];
|
||||
setPlanDetails({ ...plan, features });
|
||||
const allPlans = data?.results || [];
|
||||
|
||||
// Show all active plans (including free plan)
|
||||
const publicPlans = allPlans
|
||||
.filter((p: Plan) => p.is_active)
|
||||
.sort((a: Plan, b: Plan) => {
|
||||
const priceA = typeof a.price === 'number' ? a.price : parseFloat(String(a.price || 0));
|
||||
const priceB = typeof b.price === 'number' ? b.price : parseFloat(String(b.price || 0));
|
||||
return priceA - priceB;
|
||||
});
|
||||
|
||||
setPlans(publicPlans);
|
||||
|
||||
// Auto-select plan from URL or default to first plan
|
||||
if (planSlug) {
|
||||
const plan = publicPlans.find((p: Plan) => p.slug === planSlug);
|
||||
if (plan) {
|
||||
setSelectedPlan(plan);
|
||||
} else {
|
||||
setSelectedPlan(publicPlans[0] || null);
|
||||
}
|
||||
} else {
|
||||
setSelectedPlan(publicPlans[0] || null);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore; SignUpForm will handle lack of plan data gracefully
|
||||
console.error('Failed to load plans:', e);
|
||||
} finally {
|
||||
setPlanLoading(false);
|
||||
setPlansLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlans();
|
||||
@@ -43,9 +82,36 @@ export default function SignUp() {
|
||||
title="Sign Up - IGNY8"
|
||||
description="Create your IGNY8 account and start building topical authority with AI-powered content"
|
||||
/>
|
||||
<AuthLayout plan={planDetails}>
|
||||
<SignUpFormSimplified planDetails={planDetails} planLoading={planLoading} />
|
||||
</AuthLayout>
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="flex min-h-screen">
|
||||
{/* Left Side - Signup Form */}
|
||||
<SignUpFormUnified
|
||||
plans={plans}
|
||||
selectedPlan={selectedPlan}
|
||||
onPlanSelect={setSelectedPlan}
|
||||
plansLoading={plansLoading}
|
||||
/>
|
||||
|
||||
{/* Right Side - Pricing Plans */}
|
||||
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800 p-8 xl:p-12 items-start justify-center relative">
|
||||
{/* Logo - Top Right */}
|
||||
<Link to="/" className="absolute top-6 right-6 flex items-center gap-3">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-brand-600 dark:bg-brand-500 rounded-xl">
|
||||
<span className="text-xl font-bold text-white">I</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900 dark:text-white">TailAdmin</span>
|
||||
</Link>
|
||||
|
||||
<div className="w-full max-w-2xl mt-20">
|
||||
|
||||
{/* Pricing Plans Component Will Load Here */}
|
||||
<div id="signup-pricing-plans" className="w-full">
|
||||
{/* Plans will be rendered by SignUpFormUnified */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user