# Stripe Integration Code Snippets ## Komplette Code-Beispiele für Copy & Paste ### 1. Complete Checkout API Route ```typescript // src/routes/api/stripe/checkout/+server.ts import { json } from '@sveltejs/kit'; import Stripe from 'stripe'; import { STRIPE_SECRET_KEY, STRIPE_PRICE_MONTHLY, STRIPE_PRICE_YEARLY } from '$env/static/private'; import { PUBLIC_APP_URL } from '$env/static/public'; import type { RequestHandler } from './$types'; const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2024-11-20', }); export const POST: RequestHandler = async ({ request, locals }) => { try { // Verify authentication if (!locals.pb.authStore.isValid) { return json({ error: 'Bitte erst einloggen' }, { status: 401 }); } const user = locals.pb.authStore.model; const { interval = 'month' } = await request.json(); // Check if user already has active subscription if (user?.subscription_status === 'pro') { return json({ error: 'Du hast bereits ein aktives Abo' }, { status: 400 }); } // Create or get Stripe customer let stripeCustomerId = user?.stripe_customer_id; if (!stripeCustomerId) { const customer = await stripe.customers.create({ email: user.email, name: user.name || undefined, metadata: { pocketbase_id: user.id, username: user.username || '', }, }); stripeCustomerId = customer.id; // Save customer ID for future use await locals.pb.collection('users').update(user.id, { stripe_customer_id: stripeCustomerId, }); } // Choose price based on interval const priceId = interval === 'year' ? STRIPE_PRICE_YEARLY : STRIPE_PRICE_MONTHLY; // Create Stripe Checkout session const session = await stripe.checkout.sessions.create({ customer: stripeCustomerId, payment_method_types: ['card', 'sepa_debit', 'paypal'], billing_address_collection: 'required', line_items: [ { price: priceId, quantity: 1, }, ], mode: 'subscription', allow_promotion_codes: true, subscription_data: { metadata: { pocketbase_user_id: user.id, }, }, success_url: `${PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${PUBLIC_APP_URL}/pricing?cancelled=true`, locale: 'de', metadata: { user_id: user.id, user_email: user.email, }, }); return json({ sessionId: session.id, url: session.url, }); } catch (error) { console.error('Stripe checkout error:', error); return json( { error: 'Fehler beim Erstellen der Checkout-Session', }, { status: 500 } ); } }; ``` ### 2. Complete Webhook Handler ```typescript // src/routes/api/stripe/webhook/+server.ts import Stripe from 'stripe'; import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '$env/static/private'; import type { RequestHandler } from './$types'; const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2024-11-20', }); export const POST: RequestHandler = async ({ request, locals }) => { const body = await request.text(); const signature = request.headers.get('stripe-signature'); if (!signature) { return new Response('No signature', { status: 400 }); } let event: Stripe.Event; try { event = stripe.webhooks.constructEvent(body, signature, STRIPE_WEBHOOK_SECRET); } catch (err: any) { console.error(`Webhook signature verification failed: ${err.message}`); return new Response(`Webhook Error: ${err.message}`, { status: 400 }); } // Handle different event types try { switch (event.type) { case 'checkout.session.completed': { const session = event.data.object as Stripe.Checkout.Session; console.log('✅ Checkout completed:', session.id); const userId = session.metadata?.user_id; if (!userId) { console.error('No user_id in session metadata'); break; } // Get subscription details const subscription = await stripe.subscriptions.retrieve(session.subscription as string); // Update user in PocketBase await locals.pb.collection('users').update(userId, { subscription_status: 'pro', stripe_customer_id: session.customer, stripe_subscription_id: subscription.id, current_period_end: new Date(subscription.current_period_end * 1000).toISOString(), subscription_interval: subscription.items.data[0].price.recurring?.interval || 'month', }); // Reset usage counter for new subscribers await locals.pb.collection('users').update(userId, { links_created_this_month: 0, monthly_reset_date: new Date( new Date().getFullYear(), new Date().getMonth() + 1, 1 ).toISOString(), }); console.log(`User ${userId} upgraded to Pro`); break; } case 'customer.subscription.updated': { const subscription = event.data.object as Stripe.Subscription; console.log('📝 Subscription updated:', subscription.id); // Get PocketBase user ID from customer const customer = (await stripe.customers.retrieve( subscription.customer as string )) as Stripe.Customer; const userId = customer.metadata?.pocketbase_id; if (!userId) { console.error('No pocketbase_id in customer metadata'); break; } // Map Stripe status to our status let status = 'free'; switch (subscription.status) { case 'active': status = 'pro'; break; case 'past_due': status = 'past_due'; break; case 'canceled': case 'unpaid': status = 'cancelled'; break; } // Update user subscription status await locals.pb.collection('users').update(userId, { subscription_status: status, current_period_end: new Date(subscription.current_period_end * 1000).toISOString(), }); console.log(`User ${userId} subscription status: ${status}`); break; } case 'customer.subscription.deleted': { const subscription = event.data.object as Stripe.Subscription; console.log('❌ Subscription cancelled:', subscription.id); const customer = (await stripe.customers.retrieve( subscription.customer as string )) as Stripe.Customer; const userId = customer.metadata?.pocketbase_id; if (!userId) { console.error('No pocketbase_id in customer metadata'); break; } // Downgrade user to free await locals.pb.collection('users').update(userId, { subscription_status: 'free', stripe_subscription_id: null, current_period_end: null, }); console.log(`User ${userId} downgraded to Free`); break; } case 'invoice.payment_failed': { const invoice = event.data.object as Stripe.Invoice; console.log('💳 Payment failed:', invoice.id); const customer = (await stripe.customers.retrieve( invoice.customer as string )) as Stripe.Customer; const userId = customer.metadata?.pocketbase_id; if (!userId) break; // Mark as past_due await locals.pb.collection('users').update(userId, { subscription_status: 'past_due', }); // TODO: Send email notification to user console.log(`User ${userId} payment failed`); break; } case 'invoice.payment_succeeded': { const invoice = event.data.object as Stripe.Invoice; console.log('✅ Payment succeeded:', invoice.id); // If user was past_due, reactivate const customer = (await stripe.customers.retrieve( invoice.customer as string )) as Stripe.Customer; const userId = customer.metadata?.pocketbase_id; if (!userId) break; const user = await locals.pb.collection('users').getOne(userId); if (user.subscription_status === 'past_due') { await locals.pb.collection('users').update(userId, { subscription_status: 'pro', }); console.log(`User ${userId} reactivated after payment`); } break; } default: console.log(`Unhandled event type: ${event.type}`); } return new Response('Webhook processed', { status: 200 }); } catch (error) { console.error('Webhook processing error:', error); return new Response('Webhook processing failed', { status: 500 }); } }; ``` ### 3. Customer Portal Route ```typescript // src/routes/api/stripe/portal/+server.ts import { json } from '@sveltejs/kit'; import Stripe from 'stripe'; import { STRIPE_SECRET_KEY } from '$env/static/private'; import { PUBLIC_APP_URL } from '$env/static/public'; import type { RequestHandler } from './$types'; const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2024-11-20', }); export const POST: RequestHandler = async ({ locals }) => { try { if (!locals.pb.authStore.isValid) { return json({ error: 'Nicht authentifiziert' }, { status: 401 }); } const user = locals.pb.authStore.model; if (!user?.stripe_customer_id) { return json({ error: 'Kein Stripe-Kunde gefunden' }, { status: 400 }); } // Create portal session const session = await stripe.billingPortal.sessions.create({ customer: user.stripe_customer_id, return_url: `${PUBLIC_APP_URL}/account`, locale: 'de', }); return json({ url: session.url }); } catch (error) { console.error('Portal session error:', error); return json({ error: 'Fehler beim Erstellen der Portal-Session' }, { status: 500 }); } }; ``` ### 4. Usage Tracking Service ```typescript // src/lib/server/subscription.ts import type { PocketBase } from 'pocketbase'; export class SubscriptionService { constructor(private pb: PocketBase) {} async canCreateLink(userId: string): Promise<{ allowed: boolean; reason?: string }> { const user = await this.pb.collection('users').getOne(userId); // Pro users have unlimited access if (user.subscription_status === 'pro') { return { allowed: true }; } // Past due users should pay first if (user.subscription_status === 'past_due') { return { allowed: false, reason: 'Bitte aktualisiere deine Zahlungsmethode', }; } // Check monthly limit for free users await this.checkAndResetMonthlyCounter(userId, user); const updatedUser = await this.pb.collection('users').getOne(userId); const linksUsed = updatedUser.links_created_this_month || 0; if (linksUsed >= 10) { return { allowed: false, reason: `Du hast bereits ${linksUsed} von 10 kostenlosen Links diesen Monat erstellt`, }; } return { allowed: true }; } async incrementUsage(userId: string): Promise { const user = await this.pb.collection('users').getOne(userId); // Don't count for pro users if (user.subscription_status === 'pro') return; const currentCount = user.links_created_this_month || 0; await this.pb.collection('users').update(userId, { links_created_this_month: currentCount + 1, }); // Log usage await this.pb.collection('usage_logs').create({ user: userId, action: 'link_created', timestamp: new Date().toISOString(), }); } private async checkAndResetMonthlyCounter(userId: string, user: any): Promise { const now = new Date(); const resetDate = user.monthly_reset_date ? new Date(user.monthly_reset_date) : null; // Reset if needed (first of the month or no reset date) if (!resetDate || resetDate <= now) { const nextReset = new Date(now.getFullYear(), now.getMonth() + 1, 1); await this.pb.collection('users').update(userId, { links_created_this_month: 0, monthly_reset_date: nextReset.toISOString(), }); } } async getUsageStats(userId: string): Promise<{ used: number; limit: number; unlimited: boolean; daysUntilReset: number; }> { const user = await this.pb.collection('users').getOne(userId); if (user.subscription_status === 'pro') { return { used: 0, limit: 0, unlimited: true, daysUntilReset: 0, }; } await this.checkAndResetMonthlyCounter(userId, user); const updatedUser = await this.pb.collection('users').getOne(userId); const resetDate = new Date(updatedUser.monthly_reset_date); const now = new Date(); const daysUntilReset = Math.ceil((resetDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); return { used: updatedUser.links_created_this_month || 0, limit: 10, unlimited: false, daysUntilReset, }; } } ``` ### 5. React/Svelte Components ```svelte
{#if recommended}
Empfohlen
{/if}

{title}

{price} /{interval === 'year' ? 'Jahr' : 'Monat'}
    {#each features as feature}
  • {feature}
  • {/each}
{#if error}
{error}
{/if}
``` ### 6. Hooks for Protection ```typescript // src/hooks.server.ts import type { Handle } from '@sveltejs/kit'; import { SubscriptionService } from '$lib/server/subscription'; export const handle: Handle = async ({ event, resolve }) => { // Check subscription for API routes if (event.url.pathname.startsWith('/api/links') && event.request.method === 'POST') { if (!event.locals.pb.authStore.isValid) { return new Response('Unauthorized', { status: 401 }); } const service = new SubscriptionService(event.locals.pb); const userId = event.locals.pb.authStore.model?.id; const { allowed, reason } = await service.canCreateLink(userId); if (!allowed) { return new Response( JSON.stringify({ error: reason, requiresUpgrade: true, }), { status: 403, headers: { 'Content-Type': 'application/json' }, } ); } } return resolve(event); }; ``` ### 7. Account Management Page ```svelte

Account Einstellungen

Subscription Status

Aktueller Plan
{isPro ? 'Pro' : 'Free'}
{#if nextBillingDate}
Nächste Zahlung: {nextBillingDate}
{/if}
{#if !isPro}
Links diesen Monat
{data.usage?.used || 0}/10
Reset in {data.usage?.daysUntilReset || 0} Tagen
{/if}
{#if isPro} {:else} Upgrade to Pro {/if}
``` ### 8. Testing Utilities ```typescript // src/lib/server/stripe-test.ts import type { PocketBase } from 'pocketbase'; export async function createTestSubscription(pb: PocketBase, userId: string) { // Simulate Pro subscription for testing await pb.collection('users').update(userId, { subscription_status: 'pro', stripe_customer_id: 'cus_test_' + Date.now(), stripe_subscription_id: 'sub_test_' + Date.now(), current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), }); } export async function simulateWebhook(eventType: string, data: any) { const response = await fetch('/api/stripe/webhook', { method: 'POST', headers: { 'stripe-signature': 'test_signature', 'Content-Type': 'application/json', }, body: JSON.stringify({ type: eventType, data: { object: data }, }), }); return response; } ``` ### 9. Migration Script ```typescript // scripts/migrate-to-stripe.ts import PocketBase from 'pocketbase'; const pb = new PocketBase('http://127.0.0.1:8090'); async function addStripeFields() { // Get users collection const collection = await pb.collections.getOne('users'); // Add new fields const updatedSchema = [ ...collection.schema, { name: 'subscription_status', type: 'select', options: { values: ['free', 'pro', 'cancelled', 'past_due'], }, required: true, }, { name: 'stripe_customer_id', type: 'text', required: false, }, { name: 'stripe_subscription_id', type: 'text', required: false, }, { name: 'current_period_end', type: 'date', required: false, }, { name: 'links_created_this_month', type: 'number', min: 0, required: true, }, { name: 'monthly_reset_date', type: 'date', required: false, }, ]; // Update collection await pb.collections.update('users', { schema: updatedSchema, }); console.log('✅ Migration completed'); } addStripeFields().catch(console.error); ``` ## Usage Examples ### Check subscription before action ```typescript // In your API route const service = new SubscriptionService(locals.pb); const { allowed, reason } = await service.canCreateLink(userId); if (!allowed) { return json({ error: reason, requiresUpgrade: true }, { status: 403 }); } ``` ### Display usage in UI ```svelte {#if $page.data.usage && !$page.data.usage.unlimited}
Du hast {$page.data.usage.used} von {$page.data.usage.limit} Links erstellt
{/if} ``` ### Handle upgrade flow ```typescript try { const response = await createLink(data); // Success } catch (error) { if (error.requiresUpgrade) { goto('/pricing'); } } ```