Add pricing plans and integrate Stripe for server upgrades
Add new styling and HTML structure for pricing cards to federation.html, and configure Stripe webhook handling in webServer.js to manage subscription events and update server tiers in the database. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 2926813b-ebbf-4fd8-aa3c-58eb0dbcf182 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/0iqMcgO Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
775b380a50
commit
150ec17072
3 changed files with 414 additions and 0 deletions
4
.replit
4
.replit
|
|
@ -21,6 +21,10 @@ externalPort = 80
|
|||
localPort = 8080
|
||||
externalPort = 8080
|
||||
|
||||
[[ports]]
|
||||
localPort = 39693
|
||||
externalPort = 3000
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
|
|
|
|||
|
|
@ -327,6 +327,86 @@
|
|||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pricing-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.pricing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.pricing-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pricing-card:hover {
|
||||
border-color: var(--card-border-hover);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.pricing-card.featured {
|
||||
border-color: var(--primary);
|
||||
background: linear-gradient(180deg, rgba(99, 102, 241, 0.1) 0%, var(--card) 100%);
|
||||
}
|
||||
|
||||
.pricing-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pricing-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; }
|
||||
.pricing-price { font-size: 2.5rem; font-weight: 700; color: var(--primary-light); }
|
||||
.pricing-period { font-size: 0.9rem; color: var(--muted); }
|
||||
.pricing-desc { color: var(--muted); margin: 1rem 0; font-size: 0.9rem; }
|
||||
|
||||
.pricing-features {
|
||||
list-style: none;
|
||||
text-align: left;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.pricing-features li {
|
||||
padding: 0.5rem 0;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pricing-features li::before {
|
||||
content: '✓';
|
||||
color: var(--success);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.select-wrapper select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
background: var(--secondary);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid { grid-template-columns: 1fr; }
|
||||
.nav-links { display: none; }
|
||||
|
|
@ -364,6 +444,64 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container pricing-section">
|
||||
<h2 style="text-align: center; margin-bottom: 2rem;">Upgrade Your Protection</h2>
|
||||
<div class="pricing-grid">
|
||||
<div class="pricing-card">
|
||||
<div class="pricing-title">Free</div>
|
||||
<div class="pricing-price">$0</div>
|
||||
<div class="pricing-period">forever</div>
|
||||
<div class="pricing-desc">Basic protection for all federation members</div>
|
||||
<ul class="pricing-features">
|
||||
<li>Critical threat auto-bans</li>
|
||||
<li>Server directory listing</li>
|
||||
<li>Reputation tracking</li>
|
||||
<li>Community support</li>
|
||||
</ul>
|
||||
<button class="btn btn-secondary" disabled>Current Plan</button>
|
||||
</div>
|
||||
|
||||
<div class="pricing-card featured">
|
||||
<div class="pricing-badge">RECOMMENDED</div>
|
||||
<div class="pricing-title">Premium</div>
|
||||
<div class="pricing-price">$50</div>
|
||||
<div class="pricing-period">per month</div>
|
||||
<div class="pricing-desc">Full protection from ALL threat levels</div>
|
||||
<ul class="pricing-features">
|
||||
<li>Auto-kick for ALL ban severities</li>
|
||||
<li>Real-time threat alerts</li>
|
||||
<li>Priority modlog notifications</li>
|
||||
<li>Premium support badge</li>
|
||||
</ul>
|
||||
<div class="select-wrapper">
|
||||
<select id="premiumGuildSelect">
|
||||
<option value="">Select a server...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="upgradePlan('premium')">Upgrade to Premium</button>
|
||||
</div>
|
||||
|
||||
<div class="pricing-card">
|
||||
<div class="pricing-title">Featured Slot</div>
|
||||
<div class="pricing-price">$200</div>
|
||||
<div class="pricing-period">per week</div>
|
||||
<div class="pricing-desc">Promote your server across the entire federation</div>
|
||||
<ul class="pricing-features">
|
||||
<li>Featured in all member servers</li>
|
||||
<li>Cross-server promotion</li>
|
||||
<li>Boost your member count</li>
|
||||
<li>Priority directory placement</li>
|
||||
</ul>
|
||||
<div class="select-wrapper">
|
||||
<select id="featuredGuildSelect">
|
||||
<option value="">Select a server...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="upgradePlan('featured')">Get Featured</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
|
|
@ -615,11 +753,81 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function loadUserGuilds() {
|
||||
try {
|
||||
const res = await fetch('/api/user');
|
||||
if (!res.ok) return;
|
||||
|
||||
const data = await res.json();
|
||||
if (!data.user?.guilds) return;
|
||||
|
||||
const adminGuilds = data.user.guilds.filter(g => g.isAdmin);
|
||||
|
||||
const premiumSelect = document.getElementById('premiumGuildSelect');
|
||||
const featuredSelect = document.getElementById('featuredGuildSelect');
|
||||
|
||||
adminGuilds.forEach(g => {
|
||||
const option1 = new Option(g.name, g.id);
|
||||
const option2 = new Option(g.name, g.id);
|
||||
premiumSelect.appendChild(option1);
|
||||
featuredSelect.appendChild(option2);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load user guilds:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradePlan(planType) {
|
||||
const selectId = planType === 'premium' ? 'premiumGuildSelect' : 'featuredGuildSelect';
|
||||
const guildId = document.getElementById(selectId).value;
|
||||
|
||||
if (!guildId) {
|
||||
alert('Please select a server first');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/stripe/create-checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ guildId, planType })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
if (res.status === 401) {
|
||||
alert('Please log in first using Discord');
|
||||
window.location.href = '/auth/discord';
|
||||
return;
|
||||
}
|
||||
throw new Error(error.error || 'Failed to create checkout');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (data.url) {
|
||||
window.location.href = data.url;
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to start checkout: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('success') === 'true') {
|
||||
const plan = urlParams.get('plan');
|
||||
alert(`Successfully upgraded to ${plan === 'premium' ? 'Premium' : 'Featured Slot'}! Your server is now protected.`);
|
||||
window.history.replaceState({}, '', '/federation');
|
||||
}
|
||||
if (urlParams.get('canceled') === 'true') {
|
||||
window.history.replaceState({}, '', '/federation');
|
||||
}
|
||||
|
||||
loadStats();
|
||||
loadServers();
|
||||
loadBans();
|
||||
loadApplications();
|
||||
loadLeaderboard();
|
||||
loadUserGuilds();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ const cookieParser = require('cookie-parser');
|
|||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const Stripe = require('stripe');
|
||||
|
||||
function createWebServer(discordClient, supabase, options = {}) {
|
||||
const app = express();
|
||||
|
||||
const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY) : null;
|
||||
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
|
||||
|
||||
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
|
||||
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex');
|
||||
|
|
@ -19,6 +23,97 @@ function createWebServer(discordClient, supabase, options = {}) {
|
|||
credentials: true
|
||||
}));
|
||||
app.use(cookieParser());
|
||||
|
||||
// Stripe webhook must be registered BEFORE express.json() to access raw body
|
||||
app.post('/api/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
||||
if (!stripe || !STRIPE_WEBHOOK_SECRET) {
|
||||
return res.status(503).json({ error: 'Stripe webhook not configured' });
|
||||
}
|
||||
|
||||
const sig = req.headers['stripe-signature'];
|
||||
let event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(req.body, sig, STRIPE_WEBHOOK_SECRET);
|
||||
} catch (err) {
|
||||
console.error('Webhook signature verification failed:', err.message);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed': {
|
||||
const session = event.data.object;
|
||||
const { guild_id, plan_type } = session.metadata;
|
||||
|
||||
if (plan_type === 'premium') {
|
||||
await supabase.from('federation_servers').update({
|
||||
tier: 'premium',
|
||||
subscription_id: session.subscription,
|
||||
subscription_status: 'active',
|
||||
updated_at: new Date().toISOString()
|
||||
}).eq('guild_id', guild_id);
|
||||
|
||||
console.log(`[Stripe] Guild ${guild_id} upgraded to premium`);
|
||||
} else if (plan_type === 'featured') {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + 7);
|
||||
|
||||
await supabase.from('federation_featured').upsert({
|
||||
guild_id: guild_id,
|
||||
subscription_id: session.subscription,
|
||||
active: true,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
created_at: new Date().toISOString()
|
||||
}, { onConflict: 'guild_id' });
|
||||
|
||||
console.log(`[Stripe] Guild ${guild_id} purchased featured slot`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.updated': {
|
||||
const subscription = event.data.object;
|
||||
const status = subscription.status;
|
||||
|
||||
if (status === 'active') {
|
||||
await supabase.from('federation_servers').update({
|
||||
subscription_status: 'active'
|
||||
}).eq('subscription_id', subscription.id);
|
||||
} else if (status === 'past_due' || status === 'unpaid') {
|
||||
await supabase.from('federation_servers').update({
|
||||
subscription_status: status
|
||||
}).eq('subscription_id', subscription.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object;
|
||||
|
||||
await supabase.from('federation_servers').update({
|
||||
tier: 'free',
|
||||
subscription_id: null,
|
||||
subscription_status: null,
|
||||
updated_at: new Date().toISOString()
|
||||
}).eq('subscription_id', subscription.id);
|
||||
|
||||
await supabase.from('federation_featured').update({
|
||||
active: false
|
||||
}).eq('subscription_id', subscription.id);
|
||||
|
||||
console.log(`[Stripe] Subscription ${subscription.id} canceled`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error('Webhook processing error:', error);
|
||||
res.status(500).json({ error: 'Webhook processing failed' });
|
||||
}
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
|
|
@ -1224,6 +1319,113 @@ function createWebServer(discordClient, supabase, options = {}) {
|
|||
}
|
||||
});
|
||||
|
||||
// ============ STRIPE PAYMENT API ============
|
||||
|
||||
app.post('/api/stripe/create-checkout', async (req, res) => {
|
||||
if (!stripe) {
|
||||
return res.status(503).json({ error: 'Stripe not configured' });
|
||||
}
|
||||
|
||||
const userId = req.session.user?.id;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { guildId, planType } = req.body;
|
||||
|
||||
if (!guildId || !planType) {
|
||||
return res.status(400).json({ error: 'Missing guildId or planType' });
|
||||
}
|
||||
|
||||
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
|
||||
if (!userGuild || !userGuild.isAdmin) {
|
||||
return res.status(403).json({ error: 'Must be server admin to purchase' });
|
||||
}
|
||||
|
||||
try {
|
||||
const prices = {
|
||||
premium: {
|
||||
amount: 5000,
|
||||
name: 'Federation Premium',
|
||||
description: 'Full protection from all ban severity levels - auto-kick for all threats',
|
||||
recurring: { interval: 'month' }
|
||||
},
|
||||
featured: {
|
||||
amount: 20000,
|
||||
name: 'Featured Server Slot',
|
||||
description: 'Cross-promotion across all federation servers for 1 week',
|
||||
recurring: { interval: 'week' }
|
||||
}
|
||||
};
|
||||
|
||||
const plan = prices[planType];
|
||||
if (!plan) {
|
||||
return res.status(400).json({ error: 'Invalid plan type' });
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
payment_method_types: ['card'],
|
||||
mode: 'subscription',
|
||||
line_items: [{
|
||||
price_data: {
|
||||
currency: 'usd',
|
||||
product_data: {
|
||||
name: plan.name,
|
||||
description: plan.description
|
||||
},
|
||||
unit_amount: plan.amount,
|
||||
recurring: plan.recurring
|
||||
},
|
||||
quantity: 1
|
||||
}],
|
||||
metadata: {
|
||||
guild_id: guildId,
|
||||
user_id: userId,
|
||||
plan_type: planType
|
||||
},
|
||||
success_url: `${BASE_URL}/federation?success=true&plan=${planType}`,
|
||||
cancel_url: `${BASE_URL}/federation?canceled=true`
|
||||
});
|
||||
|
||||
res.json({ url: session.url });
|
||||
} catch (error) {
|
||||
console.error('Stripe checkout error:', error);
|
||||
res.status(500).json({ error: 'Failed to create checkout session' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/federation/subscription/:guildId', async (req, res) => {
|
||||
if (!supabase) {
|
||||
return res.status(503).json({ error: 'Database not available' });
|
||||
}
|
||||
|
||||
const { guildId } = req.params;
|
||||
|
||||
try {
|
||||
const { data: server } = await supabase
|
||||
.from('federation_servers')
|
||||
.select('tier, subscription_status')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
const { data: featured } = await supabase
|
||||
.from('federation_featured')
|
||||
.select('active, expires_at')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('active', true)
|
||||
.maybeSingle();
|
||||
|
||||
res.json({
|
||||
tier: server?.tier || 'free',
|
||||
subscriptionStatus: server?.subscription_status || null,
|
||||
featured: featured?.active || false,
|
||||
featuredExpiresAt: featured?.expires_at || null
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch subscription' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
|
|
|
|||
Loading…
Reference in a new issue