asdasd
This commit is contained in:
@@ -31,6 +31,14 @@ class AccountContextMiddleware(MiddlewareMixin):
|
|||||||
# First, try to get user from Django session (cookie-based auth)
|
# First, try to get user from Django session (cookie-based auth)
|
||||||
# This handles cases where frontend uses credentials: 'include' with session cookies
|
# This handles cases where frontend uses credentials: 'include' with session cookies
|
||||||
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
|
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
|
||||||
|
# Block superuser access via session on non-admin routes (JWT required)
|
||||||
|
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
||||||
|
if request.user.is_superuser and not auth_header.startswith('Bearer '):
|
||||||
|
logout(request)
|
||||||
|
return JsonResponse(
|
||||||
|
{'success': False, 'error': 'Session authentication not allowed for API. Use JWT.'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
# User is authenticated via session - refresh from DB to get latest account/plan data
|
# User is authenticated via session - refresh from DB to get latest account/plan data
|
||||||
# This ensures changes to account/plan are reflected immediately without re-login
|
# This ensures changes to account/plan are reflected immediately without re-login
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -288,14 +288,27 @@ class RegisterSerializer(serializers.Serializer):
|
|||||||
from igny8_core.business.billing.models import CreditTransaction
|
from igny8_core.business.billing.models import CreditTransaction
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# ALWAYS assign the existing free plan for simple signup
|
plan_slug = validated_data.get('plan_slug')
|
||||||
# No fallbacks: if free plan is misconfigured, surface error immediately
|
paid_plans = ['starter', 'growth', 'scale']
|
||||||
try:
|
|
||||||
plan = Plan.objects.get(slug='free', is_active=True)
|
if plan_slug and plan_slug in paid_plans:
|
||||||
except Plan.DoesNotExist:
|
try:
|
||||||
raise serializers.ValidationError({
|
plan = Plan.objects.get(slug=plan_slug, is_active=True)
|
||||||
"plan": "Free plan not configured. Please contact support."
|
except Plan.DoesNotExist:
|
||||||
})
|
raise serializers.ValidationError({
|
||||||
|
"plan": f"Plan '{plan_slug}' not available. Please contact support."
|
||||||
|
})
|
||||||
|
account_status = 'pending_payment'
|
||||||
|
initial_credits = 0
|
||||||
|
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()
|
||||||
|
|
||||||
# Generate account name if not provided
|
# Generate account name if not provided
|
||||||
account_name = validated_data.get('account_name')
|
account_name = validated_data.get('account_name')
|
||||||
@@ -338,32 +351,31 @@ class RegisterSerializer(serializers.Serializer):
|
|||||||
slug = f"{base_slug}-{counter}"
|
slug = f"{base_slug}-{counter}"
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
# Get trial credits from plan
|
# Create account with status and credits seeded (0 for paid pending)
|
||||||
trial_credits = plan.get_effective_credits_per_month()
|
|
||||||
|
|
||||||
# Create account with trial status and credits seeded
|
|
||||||
account = Account.objects.create(
|
account = Account.objects.create(
|
||||||
name=account_name,
|
name=account_name,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
owner=user,
|
owner=user,
|
||||||
plan=plan,
|
plan=plan,
|
||||||
credits=trial_credits, # CRITICAL: Seed initial credits
|
credits=initial_credits,
|
||||||
status='trial' # CRITICAL: Set as trial account
|
status=account_status,
|
||||||
|
payment_method=validated_data.get('payment_method') or 'bank_transfer',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Log initial credit transaction for transparency
|
# Log initial credit transaction only for free/trial accounts with credits
|
||||||
CreditTransaction.objects.create(
|
if initial_credits > 0:
|
||||||
account=account,
|
CreditTransaction.objects.create(
|
||||||
transaction_type='subscription',
|
account=account,
|
||||||
amount=trial_credits,
|
transaction_type='subscription',
|
||||||
balance_after=trial_credits,
|
amount=initial_credits,
|
||||||
description=f'Free plan credits from {plan.name}',
|
balance_after=initial_credits,
|
||||||
metadata={
|
description=f'Free plan credits from {plan.name}',
|
||||||
'plan_slug': plan.slug,
|
metadata={
|
||||||
'registration': True,
|
'plan_slug': plan.slug,
|
||||||
'trial': True
|
'registration': True,
|
||||||
}
|
'trial': True
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Update user to reference the new account
|
# Update user to reference the new account
|
||||||
user.account = account
|
user.account = account
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ class SiteViewSet(AccountModelViewSet):
|
|||||||
"""ViewSet for managing Sites."""
|
"""ViewSet for managing Sites."""
|
||||||
serializer_class = SiteSerializer
|
serializer_class = SiteSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
"""Allow normal users (viewer) to create sites, but require editor+ for other operations."""
|
"""Allow normal users (viewer) to create sites, but require editor+ for other operations."""
|
||||||
@@ -721,7 +721,7 @@ class SectorViewSet(AccountModelViewSet):
|
|||||||
"""ViewSet for managing Sectors."""
|
"""ViewSet for managing Sectors."""
|
||||||
serializer_class = SectorSerializer
|
serializer_class = SectorSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Return sectors from sites accessible to the current user."""
|
"""Return sectors from sites accessible to the current user."""
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class CreditBalanceViewSet(viewsets.ViewSet):
|
|||||||
Unified API Standard v1.0 compliant
|
Unified API Standard v1.0 compliant
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
throttle_scope = 'billing'
|
throttle_scope = 'billing'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ class CreditUsageViewSet(AccountModelViewSet):
|
|||||||
queryset = CreditUsageLog.objects.all()
|
queryset = CreditUsageLog.objects.all()
|
||||||
serializer_class = CreditUsageLogSerializer
|
serializer_class = CreditUsageLogSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
pagination_class = CustomPageNumberPagination
|
pagination_class = CustomPageNumberPagination
|
||||||
throttle_scope = 'billing'
|
throttle_scope = 'billing'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
@@ -389,7 +389,7 @@ class CreditTransactionViewSet(AccountModelViewSet):
|
|||||||
queryset = CreditTransaction.objects.all()
|
queryset = CreditTransaction.objects.all()
|
||||||
serializer_class = CreditTransactionSerializer
|
serializer_class = CreditTransactionSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
pagination_class = CustomPageNumberPagination
|
pagination_class = CustomPageNumberPagination
|
||||||
throttle_scope = 'billing'
|
throttle_scope = 'billing'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
@@ -409,7 +409,7 @@ class CreditTransactionViewSet(AccountModelViewSet):
|
|||||||
class BillingOverviewViewSet(viewsets.ViewSet):
|
class BillingOverviewViewSet(viewsets.ViewSet):
|
||||||
"""User-facing billing overview API"""
|
"""User-facing billing overview API"""
|
||||||
permission_classes = [IsAuthenticatedAndActive]
|
permission_classes = [IsAuthenticatedAndActive]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
|
|
||||||
def account_balance(self, request):
|
def account_balance(self, request):
|
||||||
"""Get account balance with subscription info"""
|
"""Get account balance with subscription info"""
|
||||||
@@ -445,7 +445,7 @@ class BillingOverviewViewSet(viewsets.ViewSet):
|
|||||||
class AdminBillingViewSet(viewsets.ViewSet):
|
class AdminBillingViewSet(viewsets.ViewSet):
|
||||||
"""Admin-only billing management API"""
|
"""Admin-only billing management API"""
|
||||||
permission_classes = [IsAuthenticatedAndActive, permissions.IsAdminUser]
|
permission_classes = [IsAuthenticatedAndActive, permissions.IsAdminUser]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
|
|
||||||
def stats(self, request):
|
def stats(self, request):
|
||||||
"""Get system-wide billing statistics"""
|
"""Get system-wide billing statistics"""
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class SystemSettingsViewSet(AccountModelViewSet):
|
|||||||
queryset = SystemSettings.objects.all()
|
queryset = SystemSettings.objects.all()
|
||||||
serializer_class = SystemSettingsSerializer
|
serializer_class = SystemSettingsSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
pagination_class = CustomPageNumberPagination
|
pagination_class = CustomPageNumberPagination
|
||||||
throttle_scope = 'system'
|
throttle_scope = 'system'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
@@ -87,7 +87,7 @@ class AccountSettingsViewSet(AccountModelViewSet):
|
|||||||
queryset = AccountSettings.objects.all()
|
queryset = AccountSettings.objects.all()
|
||||||
serializer_class = AccountSettingsSerializer
|
serializer_class = AccountSettingsSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
pagination_class = CustomPageNumberPagination
|
pagination_class = CustomPageNumberPagination
|
||||||
throttle_scope = 'system'
|
throttle_scope = 'system'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
@@ -147,7 +147,7 @@ class UserSettingsViewSet(AccountModelViewSet):
|
|||||||
queryset = UserSettings.objects.all()
|
queryset = UserSettings.objects.all()
|
||||||
serializer_class = UserSettingsSerializer
|
serializer_class = UserSettingsSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
pagination_class = CustomPageNumberPagination
|
pagination_class = CustomPageNumberPagination
|
||||||
throttle_scope = 'system'
|
throttle_scope = 'system'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
@@ -213,7 +213,7 @@ class ModuleSettingsViewSet(AccountModelViewSet):
|
|||||||
queryset = ModuleSettings.objects.all()
|
queryset = ModuleSettings.objects.all()
|
||||||
serializer_class = ModuleSettingsSerializer
|
serializer_class = ModuleSettingsSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
pagination_class = CustomPageNumberPagination
|
pagination_class = CustomPageNumberPagination
|
||||||
throttle_scope = 'system'
|
throttle_scope = 'system'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
@@ -301,7 +301,7 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = ModuleEnableSettings.objects.all()
|
queryset = ModuleEnableSettings.objects.all()
|
||||||
serializer_class = ModuleEnableSettingsSerializer
|
serializer_class = ModuleEnableSettingsSerializer
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
throttle_scope = 'system'
|
throttle_scope = 'system'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
|
|
||||||
@@ -466,7 +466,7 @@ class AISettingsViewSet(AccountModelViewSet):
|
|||||||
queryset = AISettings.objects.all()
|
queryset = AISettings.objects.all()
|
||||||
serializer_class = AISettingsSerializer
|
serializer_class = AISettingsSerializer
|
||||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||||
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
pagination_class = CustomPageNumberPagination
|
pagination_class = CustomPageNumberPagination
|
||||||
throttle_scope = 'system'
|
throttle_scope = 'system'
|
||||||
throttle_classes = [DebugScopedRateThrottle]
|
throttle_classes = [DebugScopedRateThrottle]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useAuthStore } from "./store/authStore";
|
|||||||
// Auth pages - loaded immediately (needed for login)
|
// Auth pages - loaded immediately (needed for login)
|
||||||
import SignIn from "./pages/AuthPages/SignIn";
|
import SignIn from "./pages/AuthPages/SignIn";
|
||||||
import SignUp from "./pages/AuthPages/SignUp";
|
import SignUp from "./pages/AuthPages/SignUp";
|
||||||
|
import Payment from "./pages/Payment";
|
||||||
import NotFound from "./pages/OtherPage/NotFound";
|
import NotFound from "./pages/OtherPage/NotFound";
|
||||||
|
|
||||||
// Lazy load all other pages - only loads when navigated to
|
// Lazy load all other pages - only loads when navigated to
|
||||||
@@ -186,6 +187,7 @@ export default function App() {
|
|||||||
{/* Auth Routes - Public */}
|
{/* Auth Routes - Public */}
|
||||||
<Route path="/signin" element={<SignIn />} />
|
<Route path="/signin" element={<SignIn />} />
|
||||||
<Route path="/signup" element={<SignUp />} />
|
<Route path="/signup" element={<SignUp />} />
|
||||||
|
<Route path="/payment" element={<Payment />} />
|
||||||
|
|
||||||
{/* Protected Routes - Require Authentication */}
|
{/* Protected Routes - Require Authentication */}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "../../icons";
|
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "../../icons";
|
||||||
import Label from "../form/Label";
|
import Label from "../form/Label";
|
||||||
@@ -21,6 +21,15 @@ export default function SignUpForm() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { register, loading } = useAuthStore();
|
const { register, loading } = useAuthStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const planSlug = params.get("plan");
|
||||||
|
const paidPlans = ["starter", "growth", "scale"];
|
||||||
|
if (planSlug && paidPlans.includes(planSlug)) {
|
||||||
|
navigate(`/payment?plan=${planSlug}`, { replace: true });
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
@@ -79,7 +88,7 @@ export default function SignUpForm() {
|
|||||||
Start Your Free Trial
|
Start Your Free Trial
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
No credit card required. 2,000 AI credits to get started.
|
No credit card required. 100 AI credits to get started.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const Pricing: React.FC = () => {
|
|||||||
const tiers = [
|
const tiers = [
|
||||||
{
|
{
|
||||||
name: "Starter",
|
name: "Starter",
|
||||||
|
slug: "starter",
|
||||||
price: "$89",
|
price: "$89",
|
||||||
cadence: "per month",
|
cadence: "per month",
|
||||||
description: "For small teams starting workflows.",
|
description: "For small teams starting workflows.",
|
||||||
@@ -67,6 +68,7 @@ const Pricing: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Growth",
|
name: "Growth",
|
||||||
|
slug: "growth",
|
||||||
price: "$139",
|
price: "$139",
|
||||||
cadence: "per month",
|
cadence: "per month",
|
||||||
description: "For teams automating multiple workflows.",
|
description: "For teams automating multiple workflows.",
|
||||||
@@ -93,6 +95,7 @@ const Pricing: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Scale",
|
name: "Scale",
|
||||||
|
slug: "scale",
|
||||||
price: "$229",
|
price: "$229",
|
||||||
cadence: "per month",
|
cadence: "per month",
|
||||||
description: "For publishers and large orgs needing deeper control.",
|
description: "For publishers and large orgs needing deeper control.",
|
||||||
@@ -305,14 +308,14 @@ const Pricing: React.FC = () => {
|
|||||||
{/* CTA Button */}
|
{/* CTA Button */}
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<a
|
<a
|
||||||
href="https://app.igny8.com/signup"
|
href={`https://app.igny8.com/payment?plan=${tier.slug}`}
|
||||||
className={`inline-flex w-full items-center justify-center rounded-full px-6 py-3 text-sm font-semibold transition ${
|
className={`inline-flex w-full items-center justify-center rounded-full px-6 py-3 text-sm font-semibold transition ${
|
||||||
tier.featured
|
tier.featured
|
||||||
? "bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] text-white hover:from-[#0472b8] hover:to-[#0693e3] shadow-lg shadow-[#0693e3]/30"
|
? "bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)] text-white hover:from-[#0472b8] hover:to-[#0693e3] shadow-lg shadow-[#0693e3]/30"
|
||||||
: "border-2 border-slate-300 bg-white/50 backdrop-blur-sm text-slate-900 hover:border-[var(--color-primary)] hover:bg-white"
|
: "border-2 border-slate-300 bg-white/50 backdrop-blur-sm text-slate-900 hover:border-[var(--color-primary)] hover:bg-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Start free trial
|
{tier.price === "Free" ? "Start free trial" : `Get ${tier.name} - ${tier.price}/mo`}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
89
frontend/src/pages/Payment.tsx
Normal file
89
frontend/src/pages/Payment.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useLocation, useNavigate, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const PLAN_COPY: Record<string, { name: string; price: string; credits: string }> = {
|
||||||
|
starter: { name: "Starter", price: "$89/mo", credits: "1,000 credits/month" },
|
||||||
|
growth: { name: "Growth", price: "$139/mo", credits: "2,000 credits/month" },
|
||||||
|
scale: { name: "Scale", price: "$229/mo", credits: "4,000 credits/month" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Payment() {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [contactEmail, setContactEmail] = useState("");
|
||||||
|
const [note, setNote] = useState("");
|
||||||
|
|
||||||
|
const planSlug = useMemo(() => new URLSearchParams(location.search).get("plan") || "", [location.search]);
|
||||||
|
const plan = planSlug ? PLAN_COPY[planSlug] : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!plan) {
|
||||||
|
navigate("/pricing", { replace: true });
|
||||||
|
}
|
||||||
|
}, [plan, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-slate-50 px-4 py-16">
|
||||||
|
<div className="w-full max-w-3xl bg-white rounded-2xl shadow-lg p-8 space-y-6 border border-slate-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-slate-500">Confirm your plan</p>
|
||||||
|
<h1 className="text-2xl font-semibold text-slate-900">Complete your subscription</h1>
|
||||||
|
</div>
|
||||||
|
<Link to="/pricing" className="text-sm text-blue-600 hover:text-blue-700">
|
||||||
|
Change plan
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{plan && (
|
||||||
|
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-900">{plan.name}</h2>
|
||||||
|
<p className="text-slate-700">{plan.price}</p>
|
||||||
|
<p className="text-sm text-slate-600">{plan.credits}</p>
|
||||||
|
<p className="text-xs text-amber-700 mt-2">
|
||||||
|
Payment is completed offline (bank transfer). Submit your email below and we will send payment instructions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="block text-sm font-medium text-slate-800">
|
||||||
|
Contact email
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={contactEmail}
|
||||||
|
onChange={(e) => setContactEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-slate-900 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="block text-sm font-medium text-slate-800">
|
||||||
|
Notes (optional)
|
||||||
|
<textarea
|
||||||
|
value={note}
|
||||||
|
onChange={(e) => setNote(e.target.value)}
|
||||||
|
placeholder="Company name, billing contact, or questions"
|
||||||
|
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-slate-900 focus:border-blue-500 focus:outline-none"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link to="/signup" className="text-sm text-slate-600 hover:text-slate-800">
|
||||||
|
Prefer the free plan? Start your trial
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href={`mailto:sales@igny8.com?subject=Subscribe to ${plan?.name || ""}&body=Plan: ${
|
||||||
|
plan?.name || ""
|
||||||
|
}%0AEmail: ${encodeURIComponent(contactEmail)}%0ANotes: ${encodeURIComponent(note)}`}
|
||||||
|
className="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Request payment instructions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -114,6 +114,11 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
|
// Clear cookies (session contamination protection)
|
||||||
|
document.cookie.split(";").forEach((c) => {
|
||||||
|
document.cookie = `${c.split("=")[0].trim()}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/`;
|
||||||
|
});
|
||||||
|
localStorage.clear();
|
||||||
set({ user: null, token: null, refreshToken: null, isAuthenticated: false, loading: false });
|
set({ user: null, token: null, refreshToken: null, isAuthenticated: false, loading: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user