payment gateways and plans billing and signup pages refactored

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 13:02:53 +00:00
parent ad1756c349
commit ad75fa031e
17 changed files with 4587 additions and 500 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import PageMeta from "../../components/common/PageMeta";
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
@@ -19,7 +19,6 @@ interface Plan {
}
export default function SignUp() {
const navigate = useNavigate();
const planSlug = useMemo(() => {
const params = new URLSearchParams(window.location.search);
return params.get("plan") || "";
@@ -28,38 +27,10 @@ export default function SignUp() {
const [plans, setPlans] = useState<Plan[]>([]);
const [plansLoading, setPlansLoading] = useState(true);
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
const [geoChecked, setGeoChecked] = useState(false);
// Check geo location and redirect PK users to /signup/pk
// Using free public API: https://api.country.is (no signup required, CORS enabled)
useEffect(() => {
const checkGeoAndRedirect = async () => {
try {
// Free public geo API - no signup required
const response = await fetch('https://api.country.is/', {
signal: AbortSignal.timeout(3000), // 3 second timeout
});
if (response.ok) {
const data = await response.json();
const countryCode = data?.country;
if (countryCode === 'PK') {
// Preserve query params when redirecting
const queryString = window.location.search;
navigate(`/signup/pk${queryString}`, { replace: true });
return;
}
}
} catch (err) {
// Silently fail - continue with global signup
console.log('Geo detection failed, using global signup');
}
setGeoChecked(true);
};
checkGeoAndRedirect();
}, [navigate]);
// NOTE: Geo detection removed per payment system refactor
// Country is now selected via dropdown in the signup form
// Payment method selection happens on /account/plans after registration
useEffect(() => {
const fetchPlans = async () => {
@@ -101,15 +72,6 @@ export default function SignUp() {
fetchPlans();
}, [planSlug]);
// Don't render until geo check is complete (prevents flash)
if (!geoChecked) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500"></div>
</div>
);
}
return (
<>
<PageMeta

View File

@@ -1,116 +0,0 @@
/**
* SignUpPK - Pakistan-specific signup page
* Shows Credit/Debit Card + Bank Transfer payment options
*
* Route: /signup/pk
*/
import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import PageMeta from "../../components/common/PageMeta";
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_ahrefs_queries: number;
included_credits: number;
features: string[];
}
export default function SignUpPK() {
const planSlug = useMemo(() => {
const params = new URLSearchParams(window.location.search);
return params.get("plan") || "";
}, []);
const [plans, setPlans] = useState<Plan[]>([]);
const [plansLoading, setPlansLoading] = useState(true);
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
useEffect(() => {
const fetchPlans = async () => {
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 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) {
console.error('Failed to load plans:', e);
} finally {
setPlansLoading(false);
}
};
fetchPlans();
}, [planSlug]);
return (
<>
<PageMeta
title="Sign Up (Pakistan) - IGNY8"
description="Create your IGNY8 account and start building topical authority with AI-powered content"
/>
<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 with Pakistan-specific payment options */}
<SignUpFormUnified
plans={plans}
selectedPlan={selectedPlan}
onPlanSelect={setSelectedPlan}
plansLoading={plansLoading}
countryCode="PK"
/>
{/* Right Side - Pricing Plans */}
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-brand-50 to-purple-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">
<img
src="/images/logo/IGNY8_LIGHT_LOGO.png"
alt="IGNY8"
className="h-12 w-auto"
/>
</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>
</>
);
}

View File

@@ -67,6 +67,7 @@ import {
} from '../../services/billing.api';
import { useAuthStore } from '../../store/authStore';
import PayInvoiceModal from '../../components/billing/PayInvoiceModal';
import PendingPaymentView from '../../components/billing/PendingPaymentView';
export default function PlansAndBillingPage() {
const { startLoading, stopLoading } = usePageLoading();
@@ -385,6 +386,15 @@ export default function PlansAndBillingPage() {
const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');
const hasPendingInvoice = invoices.some((inv) => inv.status === 'pending');
// Detect new user pending payment scenario:
// - account status is 'pending_payment'
// - user has never made a successful payment
const accountStatus = user?.account?.status || '';
const hasEverPaid = payments.some((p) => p.status === 'succeeded' || p.status === 'completed');
const isNewUserPendingPayment = accountStatus === 'pending_payment' && !hasEverPaid;
const pendingInvoice = invoices.find((inv) => inv.status === 'pending');
const billingCountry = (user?.account as any)?.billing_country || 'US';
// Combined check: disable Buy Credits if no active plan OR has pending invoice
const canBuyCredits = hasActivePlan && !hasPendingInvoice;
@@ -399,6 +409,29 @@ export default function PlansAndBillingPage() {
return price > 0 && p.id !== effectivePlanId;
}).sort((a, b) => (Number(a.price) || 0) - (Number(b.price) || 0));
// NEW USER PENDING PAYMENT - Show full-page payment view
// This is the simplified flow for users who just signed up with a paid plan
if (isNewUserPendingPayment && pendingInvoice) {
const planName = currentPlan?.name || pendingInvoice.subscription?.plan?.name || 'Selected Plan';
const planPrice = pendingInvoice.total_amount || pendingInvoice.total || '0';
return (
<PendingPaymentView
invoice={pendingInvoice}
userCountry={billingCountry}
planName={planName}
planPrice={planPrice}
onPaymentSuccess={() => {
// Refresh user and billing data
const { refreshUser } = useAuthStore.getState();
refreshUser().catch(() => {});
loadData();
}}
/>
);
}
// EXISTING USER - Show normal billing dashboard
return (
<>
<PageMeta title="Plans & Billing" description="Manage your subscription and payments" />