# Stripe Integration Implementation Guide für ulo.ad ## Übersicht Diese Anleitung beschreibt die Implementierung von Stripe Checkout für ulo.ad mit einem Freemium-Modell (10 kostenlose Links pro Monat, danach Pro-Abo erforderlich). ## Architektur-Entscheidungen - **Payment Method**: Stripe Checkout (gehostete Zahlungsseite) - **Subscription Model**: Freemium mit monatlichem Pro-Plan - **Enforcement**: Server-side Paywall mit Link-Counting - **Database**: PocketBase für User-Status und Usage-Tracking ## 1. Stripe Account Setup ### 1.1 Account erstellen 1. Registriere dich bei [stripe.com](https://stripe.com) 2. Aktiviere Test-Modus für Entwicklung 3. Notiere folgende Keys aus dem Dashboard: - Publishable Key: `pk_test_...` - Secret Key: `sk_test_...` ### 1.2 Webhook Endpoint konfigurieren 1. Gehe zu Dashboard → Developers → Webhooks 2. Füge Endpoint hinzu: `https://ulo.ad/api/stripe/webhook` 3. Wähle Events: - `checkout.session.completed` - `customer.subscription.updated` - `customer.subscription.deleted` - `invoice.payment_failed` 4. Notiere Webhook Secret: `whsec_...` ### 1.3 Produkte und Preise anlegen ```javascript // Im Stripe Dashboard unter "Products": Produkt: "ulo.ad Pro" Beschreibung: "Unlimited link creation and premium features" Preis 1: - Name: "Monthly" - Preis: €9.99/Monat - Price ID: price_monthly_xxx Preis 2: - Name: "Yearly" - Preis: €99/Jahr (2 Monate gratis) - Price ID: price_yearly_xxx ``` ## 2. PocketBase Schema Updates ### 2.1 Users Collection erweitern ```javascript // Neue Felder für users collection: { "subscription_status": { "type": "select", "options": ["free", "pro", "cancelled", "past_due"], "default": "free", "required": true }, "stripe_customer_id": { "type": "text", "required": false }, "stripe_subscription_id": { "type": "text", "required": false }, "current_period_end": { "type": "date", "required": false }, "links_created_this_month": { "type": "number", "default": 0, "min": 0, "required": true }, "monthly_reset_date": { "type": "date", "required": false } } ``` ### 2.2 Usage Tracking Collection ```javascript // Neue collection: usage_logs { "user": { "type": "relation", "collection": "users", "required": true }, "action": { "type": "select", "options": ["link_created", "link_deleted", "qr_generated"], "required": true }, "timestamp": { "type": "autodate", "onCreate": true }, "link_id": { "type": "relation", "collection": "links", "required": false } } ``` ## 3. Backend Implementation ### 3.1 Environment Variables ```bash # .env.local PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_PRICE_MONTHLY=price_monthly_xxx STRIPE_PRICE_YEARLY=price_yearly_xxx PUBLIC_APP_URL=http://localhost:5173 # Production: https://ulo.ad ``` ### 3.2 Stripe Client Setup ```typescript // src/lib/server/stripe.ts import Stripe from 'stripe'; import { STRIPE_SECRET_KEY } from '$env/static/private'; export const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2024-11-20', typescript: true }); ``` ### 3.3 Checkout Session API ```typescript // src/routes/api/stripe/checkout/+server.ts import { json } from '@sveltejs/kit'; import { stripe } from '$lib/server/stripe'; import { PUBLIC_APP_URL } from '$env/static/public'; import { STRIPE_PRICE_MONTHLY } from '$env/static/private'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = async ({ request, locals }) => { try { const { priceId, userId } = await request.json(); // Verify user is authenticated if (!locals.pb.authStore.isValid) { return json({ error: 'Unauthorized' }, { status: 401 }); } const user = locals.pb.authStore.model; // Create or retrieve Stripe customer let customerId = user.stripe_customer_id; if (!customerId) { const customer = await stripe.customers.create({ email: user.email, metadata: { pocketbase_id: user.id } }); customerId = customer.id; // Save customer ID to PocketBase await locals.pb.collection('users').update(user.id, { stripe_customer_id: customerId }); } // Create checkout session const session = await stripe.checkout.sessions.create({ customer: customerId, payment_method_types: ['card', 'sepa_debit'], line_items: [ { price: priceId || STRIPE_PRICE_MONTHLY, quantity: 1 } ], mode: 'subscription', success_url: `${PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${PUBLIC_APP_URL}/pricing`, metadata: { user_id: user.id }, allow_promotion_codes: true, billing_address_collection: 'auto', locale: 'de' }); return json({ sessionId: session.id, url: session.url }); } catch (error) { console.error('Checkout session error:', error); return json({ error: 'Failed to create checkout session' }, { status: 500 }); } }; ``` ### 3.4 Webhook Handler ```typescript // src/routes/api/stripe/webhook/+server.ts import { stripe } from '$lib/server/stripe'; import { STRIPE_WEBHOOK_SECRET } from '$env/static/private'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = async ({ request, locals }) => { const signature = request.headers.get('stripe-signature'); const body = await request.text(); let event; try { event = stripe.webhooks.constructEvent(body, signature!, STRIPE_WEBHOOK_SECRET); } catch (err) { console.error('Webhook signature verification failed:', err); return new Response('Webhook Error', { status: 400 }); } // Handle the event try { switch (event.type) { case 'checkout.session.completed': { const session = event.data.object; const userId = session.metadata?.user_id; if (userId) { await locals.pb.collection('users').update(userId, { subscription_status: 'pro', stripe_subscription_id: session.subscription, current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // Approximate, will be updated by subscription.updated }); } break; } case 'customer.subscription.updated': { const subscription = event.data.object; const customer = await stripe.customers.retrieve(subscription.customer as string); if (customer && !customer.deleted && customer.metadata?.pocketbase_id) { const status = subscription.status === 'active' ? 'pro' : subscription.status === 'past_due' ? 'past_due' : 'cancelled'; await locals.pb.collection('users').update(customer.metadata.pocketbase_id, { subscription_status: status, current_period_end: new Date(subscription.current_period_end * 1000) }); } break; } case 'customer.subscription.deleted': { const subscription = event.data.object; const customer = await stripe.customers.retrieve(subscription.customer as string); if (customer && !customer.deleted && customer.metadata?.pocketbase_id) { await locals.pb.collection('users').update(customer.metadata.pocketbase_id, { subscription_status: 'cancelled', stripe_subscription_id: null }); } break; } case 'invoice.payment_failed': { const invoice = event.data.object; const customer = await stripe.customers.retrieve(invoice.customer as string); if (customer && !customer.deleted && customer.metadata?.pocketbase_id) { await locals.pb.collection('users').update(customer.metadata.pocketbase_id, { subscription_status: 'past_due' }); // TODO: Send email notification } break; } } return new Response('Webhook processed', { status: 200 }); } catch (error) { console.error('Webhook processing error:', error); return new Response('Webhook processing failed', { status: 500 }); } }; ``` ## 4. Frontend Implementation ### 4.1 Upgrade Button Component ```svelte ``` ### 4.2 Paywall Banner ```svelte {#if isNearLimit}
Verifiziere deine Zahlung...
{:else}Dein Upgrade war erfolgreich.
Du wirst in wenigen Sekunden zum Dashboard weitergeleitet...
{/if}Kein Problem! Du kannst jederzeit upgraden, wenn du bereit bist. Bis dahin kannst du weiterhin 10 Links pro Monat kostenlos erstellen.