mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 17:41:26 +02:00
refactor: restructure
monorepo with apps/ and services/ directories
This commit is contained in:
parent
25824ed0ac
commit
ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions
207
apps/uload/docs/stripe/CLAUDE_CODE_MCP_SETUP.md
Normal file
207
apps/uload/docs/stripe/CLAUDE_CODE_MCP_SETUP.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
# Stripe MCP Server für Claude Code einrichten
|
||||
|
||||
## Übersicht
|
||||
|
||||
Claude Code unterstützt MCP Server auf drei Ebenen:
|
||||
|
||||
1. **Project Scope** (`.mcp.json`) - Für dieses Projekt
|
||||
2. **User Scope** (`~/.claude.json`) - Global für alle Projekte
|
||||
3. **Local Scope** - Nur für aktuelle Session
|
||||
|
||||
## ✅ Project Setup (bereits erledigt!)
|
||||
|
||||
Die `.mcp.json` Datei im Projekt wurde bereits konfiguriert:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@stripe/mcp",
|
||||
"--tools=all",
|
||||
"--api-key=${STRIPE_SECRET_KEY:-sk_test_REPLACE_WITH_YOUR_KEY}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Key setzen (2 Optionen):
|
||||
|
||||
#### Option 1: Environment Variable (empfohlen)
|
||||
|
||||
```bash
|
||||
export STRIPE_SECRET_KEY=sk_test_YOUR_ACTUAL_KEY
|
||||
```
|
||||
|
||||
#### Option 2: Direkt in .mcp.json ersetzen
|
||||
|
||||
Ersetze `sk_test_REPLACE_WITH_YOUR_KEY` mit deinem echten Key.
|
||||
|
||||
## Global Setup (für alle Projekte)
|
||||
|
||||
### 1. Globale Config erstellen
|
||||
|
||||
```bash
|
||||
# Erstelle ~/.claude.json falls nicht vorhanden
|
||||
touch ~/.claude.json
|
||||
```
|
||||
|
||||
### 2. Stripe MCP hinzufügen
|
||||
|
||||
Füge folgendes zu `~/.claude.json` hinzu:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe-global": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_YOUR_GLOBAL_KEY"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Claude Code neustarten
|
||||
|
||||
Nach Änderungen an MCP Konfigurationen solltest du Claude Code neustarten.
|
||||
|
||||
## Verfügbare Stripe Tools
|
||||
|
||||
Mit dem MCP Server hast du Zugriff auf:
|
||||
|
||||
### Customers
|
||||
|
||||
- `customers.create` - Kunden erstellen
|
||||
- `customers.read` - Kunden abrufen
|
||||
- `customers.update` - Kunden aktualisieren
|
||||
- `customers.delete` - Kunden löschen
|
||||
- `customers.list` - Alle Kunden auflisten
|
||||
|
||||
### Products & Prices
|
||||
|
||||
- `products.create` - Produkte erstellen
|
||||
- `products.update` - Produkte bearbeiten
|
||||
- `prices.create` - Preise definieren
|
||||
- `prices.list` - Preise auflisten
|
||||
|
||||
### Subscriptions
|
||||
|
||||
- `subscriptions.create` - Abos erstellen
|
||||
- `subscriptions.update` - Abos ändern
|
||||
- `subscriptions.cancel` - Abos kündigen
|
||||
- `subscriptions.list` - Abos auflisten
|
||||
|
||||
### Payments
|
||||
|
||||
- `paymentLinks.create` - Payment Links generieren
|
||||
- `checkout.sessions.create` - Checkout Sessions erstellen
|
||||
|
||||
### Invoices
|
||||
|
||||
- `invoices.read` - Rechnungen abrufen
|
||||
- `invoices.list` - Rechnungen auflisten
|
||||
|
||||
## Test-Befehle für Claude Code
|
||||
|
||||
Sage mir einfach:
|
||||
|
||||
```markdown
|
||||
"Verwende den Stripe MCP Server um ein Test-Produkt zu erstellen"
|
||||
```
|
||||
|
||||
Ich sollte antworten können mit:
|
||||
|
||||
```
|
||||
✅ Produkt erstellt: prod_xyz123
|
||||
```
|
||||
|
||||
## Für ulo.ad spezifisch
|
||||
|
||||
```markdown
|
||||
"Verwende den Stripe MCP Server um folgendes für ulo.ad zu erstellen:
|
||||
|
||||
1. Produkt 'ulo.ad Pro' mit Beschreibung
|
||||
2. Monatspreis 9,99€
|
||||
3. Jahrespreis 99€ (2 Monate gratis)
|
||||
4. Speichere alle IDs in .env.stripe"
|
||||
```
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
### Test vs Production Keys
|
||||
|
||||
- **Test Mode**: Keys beginnen mit `sk_test_`
|
||||
- **Live Mode**: Keys beginnen mit `sk_live_` oder `rk_live_` (restricted)
|
||||
|
||||
### Restricted Keys erstellen
|
||||
|
||||
Für Production solltest du einen Restricted Key verwenden:
|
||||
|
||||
1. Stripe Dashboard → API Keys → Restricted Keys
|
||||
2. Create Restricted Key
|
||||
3. Nur diese Permissions aktivieren:
|
||||
- Customers: Write
|
||||
- Products: Write
|
||||
- Prices: Write
|
||||
- Subscriptions: Write
|
||||
- Checkout Sessions: Write
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Nutze Environment Variables statt Keys direkt in Config:
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
STRIPE_SECRET_KEY=sk_test_xxx
|
||||
|
||||
# Dann in .mcp.json
|
||||
"--api-key=${STRIPE_SECRET_KEY}"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "MCP server stripe not found"
|
||||
|
||||
→ Claude Code neustarten nach Config-Änderung
|
||||
|
||||
### "Invalid API key provided"
|
||||
|
||||
→ Prüfe ob Key mit `sk_test_` oder `sk_live_` beginnt
|
||||
|
||||
### Server startet nicht
|
||||
|
||||
Test manuell:
|
||||
|
||||
```bash
|
||||
npx -y @stripe/mcp --tools=all --api-key=sk_test_YOUR_KEY
|
||||
```
|
||||
|
||||
### Permissions Error
|
||||
|
||||
→ Verwende Restricted Key mit korrekten Permissions
|
||||
|
||||
## Status Check
|
||||
|
||||
Um zu prüfen ob alles funktioniert:
|
||||
|
||||
1. **In Claude Code**: "List alle verfügbaren MCP Server"
|
||||
2. **Stripe Test**: "Verwende Stripe MCP um die API zu testen"
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. ✅ Project MCP Config (`.mcp.json`) - Bereits erledigt!
|
||||
2. ⏳ Stripe API Key in Environment setzen
|
||||
3. ⏳ Test mit einem Produkt erstellen
|
||||
4. ⏳ Alle ulo.ad Produkte automatisch anlegen lassen
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
- **Project Setup**: ✅ Fertig in `.mcp.json`
|
||||
- **Global Setup**: Optional in `~/.claude.json`
|
||||
- **API Key**: Muss noch gesetzt werden
|
||||
- **Tools**: Alle Stripe-Funktionen verfügbar
|
||||
|
||||
Nach dem Setzen des API Keys kann ich direkt mit der Stripe API arbeiten und alle Produkte, Preise und Konfigurationen für ulo.ad automatisch erstellen!
|
||||
786
apps/uload/docs/stripe/CODE_SNIPPETS.md
Normal file
786
apps/uload/docs/stripe/CODE_SNIPPETS.md
Normal file
|
|
@ -0,0 +1,786 @@
|
|||
# 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<void> {
|
||||
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<void> {
|
||||
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
|
||||
<!-- src/lib/components/PricingCard.svelte -->
|
||||
<script lang="ts">
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { PUBLIC_STRIPE_PUBLISHABLE_KEY } from '$env/static/public';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let title: string;
|
||||
export let price: string;
|
||||
export let interval: 'month' | 'year' = 'month';
|
||||
export let features: string[] = [];
|
||||
export let recommended = false;
|
||||
|
||||
let loading = false;
|
||||
let error = '';
|
||||
|
||||
async function handleCheckout() {
|
||||
loading = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stripe/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interval })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Checkout fehlgeschlagen');
|
||||
}
|
||||
|
||||
const { url, sessionId } = await response.json();
|
||||
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
} else if (sessionId) {
|
||||
const stripe = await loadStripe(PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
||||
const { error: stripeError } = await stripe!.redirectToCheckout({ sessionId });
|
||||
if (stripeError) throw stripeError;
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message;
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card {recommended ? 'border-primary border-2' : 'border'}">
|
||||
{#if recommended}
|
||||
<div class="badge badge-primary absolute -top-3 left-1/2 -translate-x-1/2">Empfohlen</div>
|
||||
{/if}
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="card-title justify-center">{title}</h2>
|
||||
|
||||
<div class="my-4 text-center">
|
||||
<span class="text-4xl font-bold">{price}</span>
|
||||
<span class="text-gray-600">/{interval === 'year' ? 'Jahr' : 'Monat'}</span>
|
||||
</div>
|
||||
|
||||
<ul class="mb-6 space-y-2">
|
||||
{#each features as feature}
|
||||
<li class="flex items-center gap-2">
|
||||
<svg class="text-success h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
{feature}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error mb-4">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="btn {recommended ? 'btn-primary' : 'btn-outline'} w-full"
|
||||
on:click={handleCheckout}
|
||||
disabled={loading}
|
||||
>
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Lädt...
|
||||
{:else}
|
||||
Jetzt upgraden
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let loadingPortal = false;
|
||||
|
||||
async function openCustomerPortal() {
|
||||
loadingPortal = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stripe/portal', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const { url } = await response.json();
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Portal error:', error);
|
||||
} finally {
|
||||
loadingPortal = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: isPro = data.user?.subscription_status === 'pro';
|
||||
$: nextBillingDate = data.user?.current_period_end
|
||||
? new Date(data.user.current_period_end).toLocaleDateString('de-DE')
|
||||
: null;
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="mb-6 text-2xl font-bold">Account Einstellungen</h1>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Subscription Status</h2>
|
||||
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Aktueller Plan</div>
|
||||
<div class="stat-value text-primary">
|
||||
{isPro ? 'Pro' : 'Free'}
|
||||
</div>
|
||||
{#if nextBillingDate}
|
||||
<div class="stat-desc">Nächste Zahlung: {nextBillingDate}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !isPro}
|
||||
<div class="stat">
|
||||
<div class="stat-title">Links diesen Monat</div>
|
||||
<div class="stat-value">{data.usage?.used || 0}/10</div>
|
||||
<div class="stat-desc">
|
||||
Reset in {data.usage?.daysUntilReset || 0} Tagen
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card-actions mt-4 justify-end">
|
||||
{#if isPro}
|
||||
<button class="btn btn-primary" on:click={openCustomerPortal} disabled={loadingPortal}>
|
||||
{#if loadingPortal}
|
||||
<span class="loading loading-spinner"></span>
|
||||
{/if}
|
||||
Abo verwalten
|
||||
</button>
|
||||
{:else}
|
||||
<a href="/pricing" class="btn btn-primary"> Upgrade to Pro </a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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}
|
||||
<div class="alert alert-info">
|
||||
Du hast {$page.data.usage.used} von {$page.data.usage.limit} Links erstellt
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
### Handle upgrade flow
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const response = await createLink(data);
|
||||
// Success
|
||||
} catch (error) {
|
||||
if (error.requiresUpgrade) {
|
||||
goto('/pricing');
|
||||
}
|
||||
}
|
||||
```
|
||||
713
apps/uload/docs/stripe/IMPLEMENTATION_GUIDE.md
Normal file
713
apps/uload/docs/stripe/IMPLEMENTATION_GUIDE.md
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
# 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
|
||||
<!-- src/lib/components/UpgradeButton.svelte -->
|
||||
<script lang="ts">
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { PUBLIC_STRIPE_PUBLISHABLE_KEY } from '$env/static/public';
|
||||
|
||||
export let priceId: string | undefined = undefined;
|
||||
export let buttonText = 'Upgrade to Pro';
|
||||
export let className = '';
|
||||
|
||||
let loading = false;
|
||||
|
||||
async function handleUpgrade() {
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
// Create checkout session
|
||||
const response = await fetch('/api/stripe/checkout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ priceId })
|
||||
});
|
||||
|
||||
const { sessionId, url } = await response.json();
|
||||
|
||||
if (url) {
|
||||
// Redirect to Stripe Checkout
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// Fallback to Stripe.js redirect
|
||||
const stripe = await loadStripe(PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
||||
await stripe?.redirectToCheckout({ sessionId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upgrade error:', error);
|
||||
// TODO: Show error toast
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={handleUpgrade}
|
||||
disabled={loading}
|
||||
class="btn btn-primary {className}"
|
||||
class:loading
|
||||
>
|
||||
{#if loading}
|
||||
Lädt...
|
||||
{:else}
|
||||
{buttonText}
|
||||
{/if}
|
||||
</button>
|
||||
```
|
||||
|
||||
### 4.2 Paywall Banner
|
||||
|
||||
```svelte
|
||||
<!-- src/lib/components/PaywallBanner.svelte -->
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import UpgradeButton from './UpgradeButton.svelte';
|
||||
|
||||
export let linksUsed = 0;
|
||||
export let maxFreeLinks = 10;
|
||||
|
||||
$: isNearLimit = linksUsed >= maxFreeLinks - 2;
|
||||
$: hasReachedLimit = linksUsed >= maxFreeLinks;
|
||||
</script>
|
||||
|
||||
{#if isNearLimit}
|
||||
<div class="alert {hasReachedLimit ? 'alert-error' : 'alert-warning'} mb-4 shadow-lg">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 flex-shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">
|
||||
{#if hasReachedLimit}
|
||||
Link-Limit erreicht!
|
||||
{:else}
|
||||
Du näherst dich deinem monatlichen Limit
|
||||
{/if}
|
||||
</h3>
|
||||
<div class="text-xs">
|
||||
Du hast {linksUsed} von {maxFreeLinks} kostenlosen Links diesen Monat verwendet.
|
||||
{#if hasReachedLimit}
|
||||
Upgrade auf Pro für unbegrenzte Links!
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<UpgradeButton buttonText="Jetzt upgraden" className="btn-sm" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## 5. Server-side Paywall Enforcement
|
||||
|
||||
### 5.1 Usage Tracking Middleware
|
||||
|
||||
```typescript
|
||||
// src/lib/server/usage.ts
|
||||
import type { PocketBase } from 'pocketbase';
|
||||
|
||||
export async function checkUsageLimit(pb: PocketBase, userId: string): Promise<boolean> {
|
||||
const user = await pb.collection('users').getOne(userId);
|
||||
|
||||
// Pro users have no limits
|
||||
if (user.subscription_status === 'pro') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check monthly reset
|
||||
const now = new Date();
|
||||
const resetDate = user.monthly_reset_date ? new Date(user.monthly_reset_date) : null;
|
||||
|
||||
if (!resetDate || resetDate < now) {
|
||||
// Reset counter at the beginning of each month
|
||||
await pb.collection('users').update(userId, {
|
||||
links_created_this_month: 0,
|
||||
monthly_reset_date: new Date(now.getFullYear(), now.getMonth() + 1, 1)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if under limit
|
||||
return user.links_created_this_month < 10;
|
||||
}
|
||||
|
||||
export async function incrementUsage(pb: PocketBase, userId: string) {
|
||||
const user = await pb.collection('users').getOne(userId);
|
||||
|
||||
await pb.collection('users').update(userId, {
|
||||
links_created_this_month: (user.links_created_this_month || 0) + 1
|
||||
});
|
||||
|
||||
// Log usage
|
||||
await pb.collection('usage_logs').create({
|
||||
user: userId,
|
||||
action: 'link_created',
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 API Route Protection
|
||||
|
||||
```typescript
|
||||
// src/routes/api/links/+server.ts (Example)
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { checkUsageLimit, incrementUsage } from '$lib/server/usage';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = locals.pb.authStore.model?.id;
|
||||
|
||||
// Check usage limit
|
||||
const canCreate = await checkUsageLimit(locals.pb, userId);
|
||||
|
||||
if (!canCreate) {
|
||||
return json(
|
||||
{
|
||||
error: 'Monthly limit reached. Please upgrade to Pro for unlimited links.',
|
||||
requiresUpgrade: true
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the link
|
||||
const data = await request.json();
|
||||
const link = await locals.pb.collection('links').create({
|
||||
...data,
|
||||
user: userId
|
||||
});
|
||||
|
||||
// Increment usage counter
|
||||
await incrementUsage(locals.pb, userId);
|
||||
|
||||
return json(link);
|
||||
} catch (error) {
|
||||
return json({ error: 'Failed to create link' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 6. Success & Cancel Pages
|
||||
|
||||
### 6.1 Success Page
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/checkout/success/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let verifying = true;
|
||||
|
||||
onMount(async () => {
|
||||
// Verify the session with backend
|
||||
const sessionId = $page.url.searchParams.get('session_id');
|
||||
|
||||
if (sessionId) {
|
||||
// Optional: Verify session with backend
|
||||
// await fetch(`/api/stripe/verify-session/${sessionId}`);
|
||||
}
|
||||
|
||||
// Refresh user data
|
||||
await fetch('/api/user/refresh');
|
||||
|
||||
verifying = false;
|
||||
|
||||
// Redirect to dashboard after 3 seconds
|
||||
setTimeout(() => {
|
||||
goto('/dashboard');
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
{#if verifying}
|
||||
<div class="loading loading-spinner loading-lg"></div>
|
||||
<p class="mt-4">Verifiziere deine Zahlung...</p>
|
||||
{:else}
|
||||
<div class="mb-4 text-6xl">🎉</div>
|
||||
<h1 class="mb-2 text-3xl font-bold">Willkommen bei ulo.ad Pro!</h1>
|
||||
<p class="mb-4 text-lg">Dein Upgrade war erfolgreich.</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
Du wirst in wenigen Sekunden zum Dashboard weitergeleitet...
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 6.2 Cancel Page
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/checkout/cancel/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import UpgradeButton from '$lib/components/UpgradeButton.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="max-w-md text-center">
|
||||
<div class="mb-4 text-6xl">🤔</div>
|
||||
<h1 class="mb-2 text-2xl font-bold">Upgrade abgebrochen</h1>
|
||||
<p class="mb-6">
|
||||
Kein Problem! Du kannst jederzeit upgraden, wenn du bereit bist. Bis dahin kannst du weiterhin
|
||||
10 Links pro Monat kostenlos erstellen.
|
||||
</p>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<button on:click={() => goto('/dashboard')} class="btn btn-outline">
|
||||
Zurück zum Dashboard
|
||||
</button>
|
||||
<UpgradeButton buttonText="Nochmal versuchen" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 7. Testing
|
||||
|
||||
### 7.1 Stripe Test Cards
|
||||
|
||||
```
|
||||
Erfolgreiche Zahlung: 4242 4242 4242 4242
|
||||
Zahlung erfordert Auth: 4000 0025 0000 3155
|
||||
Zahlung abgelehnt: 4000 0000 0000 9995
|
||||
SEPA Test IBAN: DE89 3704 0044 0532 0130 00
|
||||
```
|
||||
|
||||
### 7.2 Webhook Testing mit Stripe CLI
|
||||
|
||||
```bash
|
||||
# Install Stripe CLI
|
||||
brew install stripe/stripe-cli/stripe
|
||||
|
||||
# Login
|
||||
stripe login
|
||||
|
||||
# Forward webhooks to local server
|
||||
stripe listen --forward-to localhost:5173/api/stripe/webhook
|
||||
|
||||
# Trigger test events
|
||||
stripe trigger checkout.session.completed
|
||||
stripe trigger customer.subscription.updated
|
||||
```
|
||||
|
||||
### 7.3 Test Checklist
|
||||
|
||||
- [ ] User kann Checkout Session erstellen
|
||||
- [ ] Redirect zu Stripe funktioniert
|
||||
- [ ] Success Page wird nach Zahlung angezeigt
|
||||
- [ ] User Status wird auf "pro" gesetzt
|
||||
- [ ] Webhooks werden verarbeitet
|
||||
- [ ] Link-Limit wird für Free User enforced
|
||||
- [ ] Pro User haben kein Limit
|
||||
- [ ] Monthly Reset funktioniert
|
||||
- [ ] Subscription Cancellation wird verarbeitet
|
||||
- [ ] Failed Payments werden gehandhabt
|
||||
|
||||
## 8. Production Deployment
|
||||
|
||||
### 8.1 Environment Variables
|
||||
|
||||
```bash
|
||||
# Production .env
|
||||
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
|
||||
STRIPE_SECRET_KEY=sk_live_xxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_live_xxx
|
||||
PUBLIC_APP_URL=https://ulo.ad
|
||||
```
|
||||
|
||||
### 8.2 Webhook URL Update
|
||||
|
||||
1. Stripe Dashboard → Webhooks
|
||||
2. Update Endpoint URL zu `https://ulo.ad/api/stripe/webhook`
|
||||
3. Neues Webhook Secret notieren
|
||||
|
||||
### 8.3 SSL/HTTPS Requirement
|
||||
|
||||
Stripe requires HTTPS in production. Stelle sicher, dass deine Domain SSL-Zertifikate hat.
|
||||
|
||||
### 8.4 Monitoring
|
||||
|
||||
- Stripe Dashboard für Payment Analytics
|
||||
- PocketBase Logs für Webhook-Fehler
|
||||
- User Feedback für UX-Probleme
|
||||
|
||||
## 9. Rechtliche Anforderungen (Deutschland)
|
||||
|
||||
### 9.1 Impressum & AGB Updates
|
||||
|
||||
- Zahlungsdienstleister erwähnen
|
||||
- Widerrufsrecht für digitale Dienstleistungen
|
||||
- Preise inkl. MwSt. anzeigen
|
||||
|
||||
### 9.2 Rechnungsstellung
|
||||
|
||||
Stripe kann automatisch Rechnungen erstellen:
|
||||
|
||||
1. Dashboard → Settings → Billing → Invoices
|
||||
2. Aktiviere "Email customers their invoices"
|
||||
3. Konfiguriere Rechnungsnummer-Format
|
||||
4. Füge Steuernummer hinzu
|
||||
|
||||
### 9.3 DSGVO
|
||||
|
||||
- Erwähne Stripe in Datenschutzerklärung
|
||||
- Datenverarbeitung in den USA erwähnen
|
||||
- Auftragsverarbeitungsvertrag mit Stripe
|
||||
|
||||
## Support & Troubleshooting
|
||||
|
||||
### Häufige Probleme
|
||||
|
||||
**Webhook 400 Error**
|
||||
|
||||
- Webhook Secret prüfen
|
||||
- Body als raw text, nicht JSON parsen
|
||||
|
||||
**User Status wird nicht aktualisiert**
|
||||
|
||||
- Webhook Events in Stripe Dashboard prüfen
|
||||
- PocketBase Permissions prüfen
|
||||
|
||||
**CORS Fehler**
|
||||
|
||||
- PUBLIC_APP_URL korrekt setzen
|
||||
- Stripe Checkout erlaubt keine custom headers
|
||||
|
||||
### Hilfreiche Links
|
||||
|
||||
- [Stripe Docs](https://stripe.com/docs)
|
||||
- [Stripe Checkout Guide](https://stripe.com/docs/payments/checkout)
|
||||
- [Webhook Best Practices](https://stripe.com/docs/webhooks/best-practices)
|
||||
- [SvelteKit + Stripe Example](https://github.com/stripe-samples/subscription-use-cases/tree/main/fixed-price-subscriptions/sveltekit)
|
||||
136
apps/uload/docs/stripe/MCP_QUICK_SETUP.md
Normal file
136
apps/uload/docs/stripe/MCP_QUICK_SETUP.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Stripe MCP Server in 5 Minuten
|
||||
|
||||
## Was das ist
|
||||
|
||||
Mit dem MCP Server kann Claude direkt Stripe-Produkte, Preise und alles andere für dich anlegen. Du musst nichts manuell im Dashboard machen.
|
||||
|
||||
## Setup (3 Minuten)
|
||||
|
||||
### 1. Stripe API Key holen
|
||||
|
||||
- Gehe zu [stripe.com/dashboard](https://dashboard.stripe.com)
|
||||
- → Developers → API Keys
|
||||
- Kopiere den **Secret Key** (sk*test*...)
|
||||
|
||||
### 2. Claude Desktop Config öffnen
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
open ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
|
||||
```bash
|
||||
notepad %APPDATA%\Claude\claude_desktop_config.json
|
||||
```
|
||||
|
||||
### 3. Config einfügen
|
||||
|
||||
Ersetze die komplette Datei mit:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_DEIN_KEY_HIER"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **Ersetze `sk_test_DEIN_KEY_HIER` mit deinem echten Stripe Key!**
|
||||
|
||||
### 4. Claude neustarten
|
||||
|
||||
- Claude Desktop komplett beenden (CMD+Q / Alt+F4)
|
||||
- Claude Desktop wieder öffnen
|
||||
|
||||
## Fertig! Das war's! ✅
|
||||
|
||||
## Test (1 Minute)
|
||||
|
||||
Sage zu Claude:
|
||||
|
||||
```
|
||||
"Verwende den Stripe MCP Server um ein Produkt namens 'Test Product' für 9.99€ zu erstellen"
|
||||
```
|
||||
|
||||
Claude sollte antworten mit etwas wie:
|
||||
|
||||
```
|
||||
✅ Produkt erstellt: prod_xyz123
|
||||
✅ Preis erstellt: price_abc456 (9.99€/Monat)
|
||||
```
|
||||
|
||||
## Was Claude jetzt kann
|
||||
|
||||
```markdown
|
||||
"Erstelle ein Produkt ulo.ad Pro für 9.99€/Monat"
|
||||
"Liste alle meine Stripe Kunden"
|
||||
"Erstelle einen Test-Kunden"
|
||||
"Zeige mir alle aktiven Subscriptions"
|
||||
"Erstelle einen Payment Link für das Pro Produkt"
|
||||
```
|
||||
|
||||
## Für ulo.ad Setup
|
||||
|
||||
Sage einfach zu Claude:
|
||||
|
||||
```markdown
|
||||
"Verwende den Stripe MCP Server um folgendes zu erstellen:
|
||||
|
||||
1. Produkt 'ulo.ad Pro'
|
||||
2. Monatspreis 9,99€
|
||||
3. Jahrespreis 99€
|
||||
4. Gib mir alle IDs für meine .env Datei"
|
||||
```
|
||||
|
||||
Claude erledigt alles automatisch und gibt dir:
|
||||
|
||||
```
|
||||
STRIPE_PRODUCT_ID=prod_xyz
|
||||
STRIPE_PRICE_MONTHLY=price_abc
|
||||
STRIPE_PRICE_YEARLY=price_def
|
||||
```
|
||||
|
||||
## Troubleshooting (falls nötig)
|
||||
|
||||
### "MCP Server nicht gefunden"
|
||||
|
||||
→ Claude komplett neustarten (nicht nur Fenster schließen)
|
||||
|
||||
### "Invalid API Key"
|
||||
|
||||
→ Key muss mit `sk_test_` beginnen
|
||||
|
||||
### "Permission denied"
|
||||
|
||||
→ Prüfe ob der Key korrekt in der Config steht
|
||||
|
||||
### Config Datei existiert nicht
|
||||
|
||||
→ Erstelle sie einfach neu:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
mkdir -p ~/Library/Application\ Support/Claude
|
||||
echo '{"mcpServers":{}}' > ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
||||
```
|
||||
|
||||
## Sicherheitstipp
|
||||
|
||||
Für Production verwende einen **Restricted Key**:
|
||||
|
||||
1. Stripe Dashboard → API Keys → Restricted Keys → Create
|
||||
2. Nur diese Permissions:
|
||||
- Products: Write
|
||||
- Prices: Write
|
||||
- Customers: Read
|
||||
3. Verwende diesen Key statt dem Secret Key
|
||||
|
||||
## Das war wirklich alles! 🚀
|
||||
|
||||
Keine weitere Konfiguration nötig. Claude kann jetzt direkt mit Stripe arbeiten.
|
||||
413
apps/uload/docs/stripe/MCP_SERVER_GUIDE.md
Normal file
413
apps/uload/docs/stripe/MCP_SERVER_GUIDE.md
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
# Stripe MCP Server Integration Guide
|
||||
|
||||
## Was ist der Stripe MCP Server?
|
||||
|
||||
Der Stripe Model Context Protocol (MCP) Server ist eine offizielle Implementierung von Stripe, die es AI-Assistenten wie Claude ermöglicht, direkt mit der Stripe API zu interagieren. Dies bedeutet, dass Claude automatisch Stripe-Produkte, Preise, Kunden und Subscriptions für dich anlegen und verwalten kann.
|
||||
|
||||
## Vorteile des MCP Servers
|
||||
|
||||
### Automatisierung
|
||||
|
||||
- Claude kann selbstständig Stripe-Ressourcen erstellen (Produkte, Preise, Kunden)
|
||||
- Automatisches Setup von Subscriptions und Payment Links
|
||||
- Direkte API-Interaktion ohne manuelles Copy & Paste
|
||||
|
||||
### Zeitersparnis
|
||||
|
||||
- Keine manuelle Arbeit im Stripe Dashboard nötig
|
||||
- Claude kann alle Stripe-Objekte programmatisch erstellen
|
||||
- Sofortige Validierung und Testing möglich
|
||||
|
||||
### Fehlerreduktion
|
||||
|
||||
- Konsistente Namensgebung und Struktur
|
||||
- Automatische Verknüpfung von IDs
|
||||
- Direkte Fehlerbehandlung durch Claude
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### Option 1: Remote MCP Server (Empfohlen)
|
||||
|
||||
Stripe hostet einen offiziellen MCP Server unter `https://mcp.stripe.com`. Dies ist der einfachste Weg:
|
||||
|
||||
```json
|
||||
// claude_desktop_config.json
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.stripe.com",
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"token": "sk_test_YOUR_STRIPE_SECRET_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Lokaler MCP Server via NPX
|
||||
|
||||
Du kannst den Stripe MCP Server auch lokal ausführen:
|
||||
|
||||
```bash
|
||||
# Alle Tools aktivieren
|
||||
npx -y @stripe/mcp --tools=all --api-key=sk_test_YOUR_KEY
|
||||
|
||||
# Nur spezifische Tools
|
||||
npx -y @stripe/mcp --tools=customers.create,products.create,prices.create --api-key=sk_test_YOUR_KEY
|
||||
```
|
||||
|
||||
Für Claude Desktop Konfiguration:
|
||||
|
||||
```json
|
||||
// claude_desktop_config.json (Lokale Variante)
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_YOUR_STRIPE_SECRET_KEY"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 3: Custom Implementation
|
||||
|
||||
Für erweiterte Anpassungen kannst du einen eigenen MCP Server implementieren:
|
||||
|
||||
```typescript
|
||||
// stripe-mcp-server.ts
|
||||
import { StripeAgentToolkit } from '@stripe/agent-toolkit/modelcontextprotocol';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
const server = new StripeAgentToolkit({
|
||||
secretKey: process.env.STRIPE_SECRET_KEY!,
|
||||
configuration: {
|
||||
actions: {
|
||||
customers: { create: true, read: true, update: true },
|
||||
products: { create: true, read: true },
|
||||
prices: { create: true, read: true },
|
||||
paymentLinks: { create: true },
|
||||
subscriptions: { create: true, read: true, update: true, cancel: true },
|
||||
invoices: { read: true },
|
||||
checkout: { sessions: { create: true } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
## Konfiguration für Claude Desktop
|
||||
|
||||
### Schritt 1: Config-Datei finden
|
||||
|
||||
Die Konfigurationsdatei befindet sich unter:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
|
||||
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
### Schritt 2: Stripe Server hinzufügen
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@stripe/mcp", "--tools=all", "--api-key=sk_test_51..."]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 3: Claude Desktop neustarten
|
||||
|
||||
Nach dem Speichern der Konfiguration:
|
||||
|
||||
1. Claude Desktop komplett beenden (nicht nur schließen)
|
||||
2. Claude Desktop neu starten
|
||||
3. In den Settings sollte der Stripe Server sichtbar sein
|
||||
|
||||
## Verfügbare Tools & Aktionen
|
||||
|
||||
### Customer Management
|
||||
|
||||
- `customers.create` - Neue Kunden anlegen
|
||||
- `customers.read` - Kunden-Informationen abrufen
|
||||
- `customers.update` - Kunden-Daten aktualisieren
|
||||
- `customers.list` - Alle Kunden auflisten
|
||||
|
||||
### Product & Pricing
|
||||
|
||||
- `products.create` - Produkte erstellen
|
||||
- `products.update` - Produkte bearbeiten
|
||||
- `prices.create` - Preise definieren
|
||||
- `prices.list` - Preise auflisten
|
||||
|
||||
### Subscriptions
|
||||
|
||||
- `subscriptions.create` - Neue Abos erstellen
|
||||
- `subscriptions.update` - Abos ändern
|
||||
- `subscriptions.cancel` - Abos kündigen
|
||||
- `subscriptions.list` - Alle Abos anzeigen
|
||||
|
||||
### Payments
|
||||
|
||||
- `paymentLinks.create` - Payment Links generieren
|
||||
- `checkout.sessions.create` - Checkout Sessions erstellen
|
||||
- `invoices.read` - Rechnungen abrufen
|
||||
- `invoices.list` - Alle Rechnungen anzeigen
|
||||
|
||||
### Webhooks
|
||||
|
||||
- `webhooks.create` - Webhook Endpoints erstellen
|
||||
- `webhooks.list` - Webhooks auflisten
|
||||
|
||||
## Praktische Anwendung für ulo.ad
|
||||
|
||||
### Automatisches Setup mit Claude
|
||||
|
||||
Mit dem MCP Server kann Claude folgende Aufgaben automatisch erledigen:
|
||||
|
||||
1. **Produkte anlegen**
|
||||
|
||||
```typescript
|
||||
// Claude kann direkt ausführen:
|
||||
await stripe.products.create({
|
||||
name: 'ulo.ad Pro',
|
||||
description: 'Unlimited link creation and premium features'
|
||||
});
|
||||
```
|
||||
|
||||
2. **Preise erstellen**
|
||||
|
||||
```typescript
|
||||
// Monatlicher Preis
|
||||
await stripe.prices.create({
|
||||
product: 'prod_xxx',
|
||||
unit_amount: 999,
|
||||
currency: 'eur',
|
||||
recurring: { interval: 'month' }
|
||||
});
|
||||
```
|
||||
|
||||
3. **Webhook Endpoints konfigurieren**
|
||||
|
||||
```typescript
|
||||
await stripe.webhooks.create({
|
||||
url: 'https://ulo.ad/api/stripe/webhook',
|
||||
enabled_events: ['checkout.session.completed', 'customer.subscription.updated']
|
||||
});
|
||||
```
|
||||
|
||||
### Workflow-Beispiel
|
||||
|
||||
```markdown
|
||||
User: "Bitte erstelle die komplette Stripe-Konfiguration für ulo.ad"
|
||||
|
||||
Claude (mit MCP Server):
|
||||
|
||||
1. ✅ Erstelle Produkt "ulo.ad Pro"
|
||||
2. ✅ Erstelle Monats-Preis (9,99€)
|
||||
3. ✅ Erstelle Jahres-Preis (99€)
|
||||
4. ✅ Konfiguriere Webhook Endpoint
|
||||
5. ✅ Erstelle Test-Kunde
|
||||
6. ✅ Generiere erste Checkout Session
|
||||
7. ✅ Speichere alle IDs in .env
|
||||
```
|
||||
|
||||
## Sicherheit & Best Practices
|
||||
|
||||
### API Keys
|
||||
|
||||
**WICHTIG**: Verwende immer Restricted API Keys mit minimalen Berechtigungen:
|
||||
|
||||
```bash
|
||||
# Erstelle einen Restricted Key im Stripe Dashboard mit nur den nötigen Permissions:
|
||||
- Customers: Write
|
||||
- Products: Write
|
||||
- Prices: Write
|
||||
- Subscriptions: Write
|
||||
- Checkout Sessions: Write
|
||||
- Webhooks: Read
|
||||
```
|
||||
|
||||
### Test vs. Production
|
||||
|
||||
```json
|
||||
// Entwicklung (Test Mode)
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe-test": {
|
||||
"command": "npx",
|
||||
"args": ["@stripe/mcp", "--api-key=sk_test_..."]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Production (Live Mode) - Vorsicht!
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe-live": {
|
||||
"command": "npx",
|
||||
"args": ["@stripe/mcp", "--api-key=rk_live_..."] // Restricted Key!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Umgebungsvariablen
|
||||
|
||||
Statt API Keys direkt in der Config zu speichern:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe": {
|
||||
"command": "npx",
|
||||
"args": ["@stripe/mcp", "--tools=all"],
|
||||
"env": {
|
||||
"STRIPE_API_KEY": "${STRIPE_SECRET_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server erscheint nicht in Claude
|
||||
|
||||
1. **Config-Syntax prüfen**: JSON muss valide sein
|
||||
2. **Claude komplett neustarten**: Nicht nur Fenster schließen
|
||||
3. **Logs prüfen**: `~/Library/Logs/Claude/` (macOS)
|
||||
|
||||
### Verbindungsfehler
|
||||
|
||||
```bash
|
||||
# Test ob NPX funktioniert
|
||||
npx -y @stripe/mcp --version
|
||||
|
||||
# Manuelle Installation falls nötig
|
||||
npm install -g @stripe/mcp
|
||||
```
|
||||
|
||||
### API Key Fehler
|
||||
|
||||
- Stelle sicher, dass der Key mit `sk_test_` (Test) oder `rk_live_` (Restricted Live) beginnt
|
||||
- Prüfe Permissions im Stripe Dashboard
|
||||
- Verwende niemals den publishable key (`pk_`)
|
||||
|
||||
## Integration mit ulo.ad Workflow
|
||||
|
||||
### Schritt 1: MCP Server aktivieren
|
||||
|
||||
```bash
|
||||
# In Claude Desktop Config hinzufügen
|
||||
{
|
||||
"mcpServers": {
|
||||
"stripe": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@stripe/mcp",
|
||||
"--tools=products.create,prices.create,customers.create,subscriptions.create,checkout.sessions.create",
|
||||
"--api-key=sk_test_YOUR_KEY"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Schritt 2: Claude anweisen
|
||||
|
||||
```markdown
|
||||
"Verwende den Stripe MCP Server um folgendes zu erstellen:
|
||||
|
||||
1. Produkt 'ulo.ad Pro' mit Beschreibung
|
||||
2. Monatspreis 9,99€ und Jahrespreis 99€
|
||||
3. Speichere alle IDs in einer .env.stripe Datei"
|
||||
```
|
||||
|
||||
### Schritt 3: Automatische Ausführung
|
||||
|
||||
Claude wird dann automatisch:
|
||||
|
||||
- Die Stripe API aufrufen
|
||||
- Alle Ressourcen erstellen
|
||||
- IDs und Konfiguration zurückgeben
|
||||
- Diese in deinem Projekt speichern
|
||||
|
||||
## Vorteile gegenüber manuellem Setup
|
||||
|
||||
| Manuell | Mit MCP Server |
|
||||
| ---------------------------- | -------------------------- |
|
||||
| 30+ Minuten Dashboard-Arbeit | 2 Minuten automatisch |
|
||||
| Copy & Paste von IDs | Automatische ID-Verwaltung |
|
||||
| Fehleranfällig | Konsistent & validiert |
|
||||
| Mehrere Browser-Tabs | Alles in Claude |
|
||||
| Manuelles Testing | Sofortige Validierung |
|
||||
|
||||
## Erweiterte Features
|
||||
|
||||
### Batch-Operationen
|
||||
|
||||
```typescript
|
||||
// Claude kann mehrere Operationen gleichzeitig ausführen
|
||||
const operations = [
|
||||
stripe.products.create({ name: 'Basic' }),
|
||||
stripe.products.create({ name: 'Pro' }),
|
||||
stripe.products.create({ name: 'Enterprise' })
|
||||
];
|
||||
await Promise.all(operations);
|
||||
```
|
||||
|
||||
### Conditional Logic
|
||||
|
||||
```typescript
|
||||
// Claude kann intelligente Entscheidungen treffen
|
||||
const existingProduct = await stripe.products.list({ limit: 1 });
|
||||
if (existingProduct.data.length === 0) {
|
||||
await stripe.products.create({ name: 'ulo.ad Pro' });
|
||||
}
|
||||
```
|
||||
|
||||
### Testing & Validation
|
||||
|
||||
```typescript
|
||||
// Claude kann automatisch testen
|
||||
const testSession = await stripe.checkout.sessions.create({
|
||||
mode: 'subscription',
|
||||
line_items: [{ price: priceId, quantity: 1 }],
|
||||
success_url: 'http://localhost:5173/success'
|
||||
});
|
||||
console.log('Test Checkout URL:', testSession.url);
|
||||
```
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **MCP Server einrichten**: Config-Datei bearbeiten und Claude neustarten
|
||||
2. **Test-Projekt**: Lass Claude ein komplettes Stripe-Setup erstellen
|
||||
3. **Integration**: Verbinde die erstellten Stripe-Ressourcen mit deinem Code
|
||||
4. **Automation**: Nutze Claude für wiederkehrende Stripe-Aufgaben
|
||||
|
||||
## Wichtige Links
|
||||
|
||||
- [Stripe Agent Toolkit GitHub](https://github.com/stripe/agent-toolkit)
|
||||
- [MCP Protocol Docs](https://modelcontextprotocol.io)
|
||||
- [Stripe API Dokumentation](https://stripe.com/docs/api)
|
||||
- [Claude Desktop MCP Guide](https://docs.anthropic.com/claude/docs/mcp)
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Der Stripe MCP Server transformiert die Art, wie du mit Stripe arbeitest:
|
||||
|
||||
- **Keine manuelle Dashboard-Arbeit mehr**: Alles über Claude
|
||||
- **Fehlerfreie Konfiguration**: Automatische Validierung
|
||||
- **Schnelleres Development**: Von Stunden auf Minuten
|
||||
- **Bessere Developer Experience**: Alles in einem Tool
|
||||
|
||||
Mit dem MCP Server wird Claude zu deinem persönlichen Stripe-Administrator, der alle API-Operationen für dich ausführen kann - sicher, schnell und zuverlässig.
|
||||
143
apps/uload/docs/stripe/Monetization_Overview.md
Normal file
143
apps/uload/docs/stripe/Monetization_Overview.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
Hier ist eine Übersicht
|
||||
eures Stripe-Setups und
|
||||
wie es mit der Datenbank
|
||||
zusammenarbeitet:
|
||||
|
||||
Stripe-Setup Architektur
|
||||
|
||||
1. Stripe-Konfiguration (
|
||||
src/lib/server/stripe.ts)
|
||||
|
||||
- Initialisiert Stripe
|
||||
mit API-Schlüssel aus
|
||||
Umgebungsvariablen
|
||||
- Definiert drei
|
||||
Preismodelle: Monthly,
|
||||
Yearly, Lifetime
|
||||
- Unterstützt mehrere
|
||||
Währungen (USD/EUR)
|
||||
basierend auf Locale
|
||||
|
||||
2. Pricing-Struktur
|
||||
|
||||
- Monthly: $4.99/Monat
|
||||
(4,99€)
|
||||
- Yearly: $39.99/Jahr
|
||||
(39,99€) - spart $20
|
||||
- Lifetime: $129.99
|
||||
einmalig (129,99€)
|
||||
|
||||
3. Checkout-Flow
|
||||
(src/routes/api/stripe/ch
|
||||
eckout/+server.ts)
|
||||
|
||||
Prozess:
|
||||
|
||||
1. User muss eingeloggt
|
||||
sein
|
||||
2. System prüft
|
||||
bestehende Subscription
|
||||
3. Erstellt/findet Stripe
|
||||
Customer mit Metadaten
|
||||
(pocketbase_id, username)
|
||||
4. Erstellt Checkout
|
||||
Session mit:
|
||||
|
||||
|
||||
- Lokalisation
|
||||
|
||||
(DE/EN/IT/FR/ES) - Zahlungsmethoden
|
||||
(Karte + SEPA) - Promotion Codes
|
||||
erlaubt - Metadaten für
|
||||
User-Zuordnung
|
||||
|
||||
4. Datenbank-Integration
|
||||
(PocketBase users
|
||||
Collection)
|
||||
|
||||
Stripe-relevante Felder:
|
||||
|
||||
- subscription_status:
|
||||
free|pro|team|team_plus|c
|
||||
ancelled|past_due
|
||||
- stripe_customer_id:
|
||||
Verknüpfung zum Stripe
|
||||
Customer
|
||||
- stripe_subscription_id:
|
||||
Verknüpfung zur Stripe
|
||||
Subscription
|
||||
- current_period_end:
|
||||
Ablaufdatum der aktuellen
|
||||
Periode
|
||||
- links_created_this_month:
|
||||
Limitierung für Free
|
||||
Users
|
||||
|
||||
5. Webhook-Handler
|
||||
(src/routes/api/stripe/we
|
||||
bhook/+server.ts)
|
||||
|
||||
Behandelte Events:
|
||||
|
||||
- checkout.session.comple
|
||||
ted: Aktiviert Pro-Status
|
||||
nach erfolgreicher
|
||||
Zahlung
|
||||
- customer.subscription.u
|
||||
pdated: Aktualisiert
|
||||
Subscription-Status
|
||||
- customer.subscription.d
|
||||
eleted: Downgrade zu Free
|
||||
- invoice.payment_failed:
|
||||
Setzt Status auf
|
||||
past_due
|
||||
- invoice.payment_succeed
|
||||
ed: Reaktiviert nach
|
||||
erfolgreicher Zahlung
|
||||
|
||||
Besonderheiten:
|
||||
|
||||
- Lifetime-Käufe:
|
||||
Erhalten subscription*id:
|
||||
"lifetime*" + session.id
|
||||
und Ablaufdatum 2099
|
||||
- Admin-Authentifizierung:
|
||||
Webhooks verwenden
|
||||
Admin-Credentials für
|
||||
Datenbank-Updates
|
||||
|
||||
6. Mehrsprachigkeit
|
||||
|
||||
- Vollständige
|
||||
Übersetzungen
|
||||
(DE/EN/IT/FR/ES) in
|
||||
stripe-translations.ts
|
||||
- Automatische
|
||||
Währungsauswahl (EUR für
|
||||
EU, USD für Rest)
|
||||
- Stripe Checkout in
|
||||
Benutzersprache
|
||||
|
||||
7. Sicherheit &
|
||||
Fehlerbehandlung
|
||||
|
||||
- Webhook-Signatur-Verifi
|
||||
zierung
|
||||
- Fallback für
|
||||
Entwicklungsumgebung ohne
|
||||
Webhook-Secret
|
||||
- Umfassende
|
||||
Fehlerbehandlung mit
|
||||
benutzerfreundlichen
|
||||
Meldungen
|
||||
- Admin-Authentifizierung
|
||||
für kritische
|
||||
Operationen
|
||||
|
||||
Das System ermöglicht
|
||||
nahtlose Übergänge
|
||||
zwischen Free- und
|
||||
Pro-Status basierend auf
|
||||
Stripe-Events und hält
|
||||
die PocketBase-Datenbank
|
||||
automatisch synchron.
|
||||
276
apps/uload/docs/stripe/QUICK_START.md
Normal file
276
apps/uload/docs/stripe/QUICK_START.md
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# Stripe Quick Start Guide
|
||||
|
||||
## 🚀 Schnellstart in 30 Minuten
|
||||
|
||||
Diese Anleitung bringt dich in 30 Minuten zu einer funktionierenden Stripe-Integration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Stripe Account (stripe.com)
|
||||
- SvelteKit Projekt läuft (`npm run dev`)
|
||||
- PocketBase läuft
|
||||
|
||||
## Step 1: Stripe Setup (5 Min)
|
||||
|
||||
### 1.1 Install Stripe Dependencies
|
||||
|
||||
```bash
|
||||
npm install stripe @stripe/stripe-js
|
||||
```
|
||||
|
||||
### 1.2 Get API Keys
|
||||
|
||||
1. Login bei [stripe.com/dashboard](https://dashboard.stripe.com)
|
||||
2. Kopiere Test Keys:
|
||||
- Publishable Key: `pk_test_...`
|
||||
- Secret Key: `sk_test_...`
|
||||
|
||||
### 1.3 Create Product
|
||||
|
||||
Im Stripe Dashboard:
|
||||
|
||||
1. Products → Add Product
|
||||
2. Name: "ulo.ad Pro"
|
||||
3. Price: €9.99/month
|
||||
4. Kopiere Price ID: `price_xxx`
|
||||
|
||||
## Step 2: Environment Setup (2 Min)
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51...
|
||||
STRIPE_SECRET_KEY=sk_test_51...
|
||||
STRIPE_PRICE_ID=price_1...
|
||||
PUBLIC_APP_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
## Step 3: Minimal Backend (10 Min)
|
||||
|
||||
### 3.1 Stripe Client
|
||||
|
||||
```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'
|
||||
});
|
||||
```
|
||||
|
||||
### 3.2 Checkout Endpoint
|
||||
|
||||
```typescript
|
||||
// src/routes/api/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_ID } from '$env/static/private';
|
||||
|
||||
export async function POST({ locals }) {
|
||||
// Check if user is logged in
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return json({ error: 'Please login first' }, { status: 401 });
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: STRIPE_PRICE_ID,
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
mode: 'subscription',
|
||||
success_url: `${PUBLIC_APP_URL}/success`,
|
||||
cancel_url: `${PUBLIC_APP_URL}/`,
|
||||
client_reference_id: locals.pb.authStore.model?.id
|
||||
});
|
||||
|
||||
return json({ url: session.url });
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Minimal Frontend (5 Min)
|
||||
|
||||
### 4.1 Upgrade Button
|
||||
|
||||
```svelte
|
||||
<!-- src/lib/components/QuickUpgrade.svelte -->
|
||||
<script lang="ts">
|
||||
let loading = false;
|
||||
|
||||
async function upgrade() {
|
||||
loading = true;
|
||||
const res = await fetch('/api/checkout', { method: 'POST' });
|
||||
const { url } = await res.json();
|
||||
window.location.href = url;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button on:click={upgrade} disabled={loading}>
|
||||
{loading ? 'Loading...' : 'Upgrade to Pro €9.99/mo'}
|
||||
</button>
|
||||
```
|
||||
|
||||
### 4.2 Success Page
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/success/+page.svelte -->
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => goto('/'), 3000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>🎉 Payment Successful!</h1>
|
||||
<p>Redirecting to dashboard...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Step 5: Test Payment (3 Min)
|
||||
|
||||
1. Click "Upgrade to Pro"
|
||||
2. Use test card: `4242 4242 4242 4242`
|
||||
3. Any future date, any CVC
|
||||
4. Complete payment
|
||||
5. Check Stripe Dashboard for payment
|
||||
|
||||
## Step 6: Basic Webhook (5 Min)
|
||||
|
||||
### 6.1 Get Webhook Secret
|
||||
|
||||
```bash
|
||||
# Install Stripe CLI
|
||||
brew install stripe/stripe-cli/stripe
|
||||
|
||||
# Login and forward webhooks
|
||||
stripe login
|
||||
stripe listen --forward-to localhost:5173/api/webhook
|
||||
|
||||
# Copy the webhook secret displayed
|
||||
```
|
||||
|
||||
### 6.2 Webhook Handler
|
||||
|
||||
```typescript
|
||||
// src/routes/api/webhook/+server.ts
|
||||
import { stripe } from '$lib/server/stripe';
|
||||
import { STRIPE_WEBHOOK_SECRET } from '$env/static/private';
|
||||
|
||||
export async function POST({ request, locals }) {
|
||||
const body = await request.text();
|
||||
const sig = request.headers.get('stripe-signature')!;
|
||||
|
||||
try {
|
||||
const event = stripe.webhooks.constructEvent(body, sig, STRIPE_WEBHOOK_SECRET);
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const session = event.data.object;
|
||||
const userId = session.client_reference_id;
|
||||
|
||||
// Update user to Pro in PocketBase
|
||||
if (userId) {
|
||||
await locals.pb.collection('users').update(userId, {
|
||||
subscription_status: 'pro'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('OK');
|
||||
} catch (err) {
|
||||
return new Response('Webhook Error', { status: 400 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 7: Add to PocketBase (2 Min)
|
||||
|
||||
Add field to users collection:
|
||||
|
||||
```javascript
|
||||
{
|
||||
name: "subscription_status",
|
||||
type: "select",
|
||||
options: ["free", "pro"],
|
||||
default: "free"
|
||||
}
|
||||
```
|
||||
|
||||
## Done! 🎊
|
||||
|
||||
Du hast jetzt:
|
||||
|
||||
- ✅ Funktionierende Checkout-Seite
|
||||
- ✅ Test-Zahlungen möglich
|
||||
- ✅ User-Status wird aktualisiert
|
||||
- ✅ Webhook-Integration
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Enforce Limits
|
||||
|
||||
```typescript
|
||||
// In your API routes
|
||||
if (user.subscription_status !== 'pro' && user.links_count >= 10) {
|
||||
return json({ error: 'Upgrade to Pro for unlimited links' }, { status: 403 });
|
||||
}
|
||||
```
|
||||
|
||||
### Add Customer Portal
|
||||
|
||||
```typescript
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: user.stripe_customer_id,
|
||||
return_url: `${PUBLIC_APP_URL}/account`
|
||||
});
|
||||
```
|
||||
|
||||
### Production Checklist
|
||||
|
||||
- [ ] Switch to live keys
|
||||
- [ ] Update webhook endpoint
|
||||
- [ ] Add error handling
|
||||
- [ ] Add loading states
|
||||
- [ ] Test mit echten Karten
|
||||
- [ ] Setup email notifications
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"No such price"**
|
||||
→ Check STRIPE_PRICE_ID in .env
|
||||
|
||||
**Webhook 400 Error**
|
||||
→ Wrong webhook secret, check stripe listen output
|
||||
|
||||
**User not updated**
|
||||
→ Check PocketBase permissions
|
||||
|
||||
**Checkout won't load**
|
||||
→ Check PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# Watch webhook events
|
||||
stripe listen --forward-to localhost:5173/api/webhook
|
||||
|
||||
# Trigger test events
|
||||
stripe trigger checkout.session.completed
|
||||
|
||||
# See recent events
|
||||
stripe events list
|
||||
|
||||
# Create test customer
|
||||
stripe customers create --email=test@example.com
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Stripe Test Cards](https://stripe.com/docs/testing#cards)
|
||||
- [Checkout Docs](https://stripe.com/docs/payments/checkout)
|
||||
- [SvelteKit Example](https://github.com/stripe-samples/checkout-single-subscription)
|
||||
327
apps/uload/docs/stripe/STRIPE_SETUP_SIMPLEST_WAY.md
Normal file
327
apps/uload/docs/stripe/STRIPE_SETUP_SIMPLEST_WAY.md
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
# Der einfachste Weg: Stripe in 1 Stunde integrieren
|
||||
|
||||
## Das Ziel
|
||||
|
||||
10 kostenlose Links pro Monat, danach 9,99€/Monat für unbegrenzte Links.
|
||||
|
||||
## Was du brauchst (5 Min)
|
||||
|
||||
### 1. Stripe Account
|
||||
|
||||
- Gehe zu [stripe.com](https://stripe.com) → Registrieren
|
||||
- Wähle "Test Mode" (oben rechts)
|
||||
- Kopiere deine Test Keys aus dem Dashboard
|
||||
|
||||
### 2. NPM Package
|
||||
|
||||
```bash
|
||||
npm install stripe @stripe/stripe-js
|
||||
```
|
||||
|
||||
### 3. Environment Variables
|
||||
|
||||
```bash
|
||||
# .env
|
||||
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51RujJlPRtmsJbOMgKRJu4uOqOGzGXwI8FT0qwf1jJUQ0HJIDmxBR3fzJSqGhVQCJ5xAJ4jKh0U6JvfLdx76FpMGB00xQI2j4qg
|
||||
STRIPE_SECRET_KEY=sk_test_51RujJlPRtmsJbOMgPQDqEA4CBgWSGKkjlCry8nTlHs9b6xSwwh0ccj6RoaZSvl84cQ8TO28Nk0ug64fRlF978vK300EsYm8RP0
|
||||
|
||||
# Stripe Product & Prices
|
||||
STRIPE_PRODUCT_ID=prod_SrqNlCbfaaKSnk
|
||||
STRIPE_PRICE_MONTHLY=price_1Rw6hkPRtmsJbOMgdUYfj7ee
|
||||
STRIPE_PRICE_YEARLY=price_1Rw6j0PRtmsJbOMgTGrOZH2c
|
||||
STRIPE_PRICE_LIFETIME=price_1Rw6qPPRtmsJbOMgsS6nnBTM
|
||||
|
||||
PUBLIC_APP_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
## Schritt 1: Produkt in Stripe erstellen (2 Min)
|
||||
|
||||
Im Stripe Dashboard:
|
||||
|
||||
1. → Products → Add Product
|
||||
2. Name: "ulo.ad Pro"
|
||||
3. → Add Price: 9,99€ / Monthly
|
||||
4. **Kopiere die Price ID** (z.B. `price_1OxAbc...`)
|
||||
5. Füge sie in `.env.local` ein
|
||||
|
||||
## Schritt 2: Minimal Backend (10 Min)
|
||||
|
||||
### Checkout Route
|
||||
|
||||
```typescript
|
||||
// src/routes/api/stripe/checkout/+server.ts
|
||||
import { json } from '@sveltejs/kit';
|
||||
import Stripe from 'stripe';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-10-28.acacia'
|
||||
});
|
||||
|
||||
export async function POST({ request, locals, url }) {
|
||||
// User muss eingeloggt sein
|
||||
if (!locals.user) {
|
||||
return json({ error: 'Login required' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { priceType = 'monthly' } = await request.json();
|
||||
|
||||
// Preis auswählen
|
||||
let priceId;
|
||||
let mode = 'subscription';
|
||||
|
||||
switch (priceType) {
|
||||
case 'yearly':
|
||||
priceId = env.STRIPE_PRICE_YEARLY;
|
||||
break;
|
||||
case 'lifetime':
|
||||
priceId = env.STRIPE_PRICE_LIFETIME;
|
||||
mode = 'payment';
|
||||
break;
|
||||
default:
|
||||
priceId = env.STRIPE_PRICE_MONTHLY;
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer_email: locals.user.email,
|
||||
payment_method_types: ['card', 'sepa_debit'],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
mode,
|
||||
success_url: `${url.origin}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${url.origin}/pricing`,
|
||||
client_reference_id: locals.user.id
|
||||
});
|
||||
|
||||
return json({ url: session.url });
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook Handler (vereinfacht)
|
||||
|
||||
```typescript
|
||||
// src/routes/api/stripe/webhook/+server.ts
|
||||
import Stripe from 'stripe';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-10-28.acacia'
|
||||
});
|
||||
|
||||
export async function POST({ request, locals }) {
|
||||
const body = await request.text();
|
||||
|
||||
// Vereinfacht: Kein Signature Check für Test
|
||||
const event = JSON.parse(body);
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const session = event.data.object;
|
||||
const userId = session.client_reference_id;
|
||||
|
||||
// User auf Pro upgraden
|
||||
await locals.pb.collection('users').update(userId, {
|
||||
subscription_status: 'pro',
|
||||
stripe_customer_id: session.customer
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('OK');
|
||||
}
|
||||
```
|
||||
|
||||
## Schritt 3: PocketBase Update (5 Min)
|
||||
|
||||
In PocketBase Admin:
|
||||
|
||||
1. Users Collection → Edit
|
||||
2. Add Field:
|
||||
- Name: `subscription_status`
|
||||
- Type: Select
|
||||
- Options: `free`, `pro`
|
||||
- Default: `free`
|
||||
3. Add Field:
|
||||
- Name: `links_count`
|
||||
- Type: Number
|
||||
- Default: 0
|
||||
|
||||
## Schritt 4: Frontend (10 Min)
|
||||
|
||||
### Upgrade Button
|
||||
|
||||
```svelte
|
||||
<!-- src/lib/components/UpgradeButton.svelte -->
|
||||
<script>
|
||||
export let priceType = 'monthly';
|
||||
|
||||
async function upgrade() {
|
||||
const res = await fetch('/api/stripe/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ priceType })
|
||||
});
|
||||
const { url } = await res.json();
|
||||
window.location.href = url;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button on:click={upgrade} class="btn btn-primary">
|
||||
Upgrade für {priceType === 'yearly' ? '39,99€/Jahr' : '4,99€/Monat'}
|
||||
</button>
|
||||
```
|
||||
|
||||
### Paywall Check
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/create/+page.svelte -->
|
||||
<script>
|
||||
export let data;
|
||||
|
||||
$: canCreate = data.user.subscription_status === 'pro' || data.user.links_count < 10;
|
||||
</script>
|
||||
|
||||
{#if !canCreate}
|
||||
<div class="alert alert-warning">
|
||||
Du hast dein Limit von 10 kostenlosen Links erreicht.
|
||||
<UpgradeButton />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Link erstellen Form -->
|
||||
{/if}
|
||||
```
|
||||
|
||||
### Success Page
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/success/+page.svelte -->
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => goto('/'), 3000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1>🎉 Willkommen bei Pro!</h1><p>Du wirst gleich weitergeleitet...</p>
|
||||
```
|
||||
|
||||
## Schritt 5: API Protection (5 Min)
|
||||
|
||||
```typescript
|
||||
// src/routes/api/links/+server.ts
|
||||
export async function POST({ request, locals }) {
|
||||
const user = locals.user;
|
||||
|
||||
// Check limit
|
||||
if (user.subscription_status !== 'pro' && user.links_count >= 10) {
|
||||
return json({
|
||||
error: 'Limit erreicht. Bitte upgrade auf Pro.'
|
||||
}, { status: 403 });
|
||||
}
|
||||
|
||||
// Link erstellen
|
||||
const link = await locals.pb.collection('links').create({...});
|
||||
|
||||
// Counter erhöhen (nur für Free User)
|
||||
if (user.subscription_status !== 'pro') {
|
||||
await locals.pb.collection('users').update(user.id, {
|
||||
links_count: user.links_count + 1
|
||||
});
|
||||
}
|
||||
|
||||
return json(link);
|
||||
}
|
||||
```
|
||||
|
||||
## Schritt 6: Testing (5 Min)
|
||||
|
||||
### Webhook mit Stripe CLI testen
|
||||
|
||||
```bash
|
||||
# Stripe CLI installieren
|
||||
brew install stripe/stripe-cli/stripe
|
||||
|
||||
# Webhooks forwarden
|
||||
stripe listen --forward-to localhost:5173/api/webhook
|
||||
|
||||
# In neuem Terminal: Test Event senden
|
||||
stripe trigger checkout.session.completed
|
||||
```
|
||||
|
||||
### Test Kreditkarte
|
||||
|
||||
- Nummer: `4242 4242 4242 4242`
|
||||
- Datum: Beliebig in Zukunft
|
||||
- CVC: Beliebige 3 Zahlen
|
||||
|
||||
## Fertig! ✅
|
||||
|
||||
Das war's. Du hast jetzt:
|
||||
|
||||
- ✅ Stripe Checkout für Payments
|
||||
- ✅ 10 kostenlose Links pro Monat
|
||||
- ✅ Pro-Abo für unbegrenzte Links
|
||||
- ✅ Automatisches Status-Update
|
||||
|
||||
## Nächste Schritte (Optional)
|
||||
|
||||
### Webhook Security hinzufügen
|
||||
|
||||
```typescript
|
||||
// Webhook Signature verifizieren
|
||||
const sig = request.headers.get('stripe-signature');
|
||||
const event = stripe.webhooks.constructEvent(body, sig, WEBHOOK_SECRET);
|
||||
```
|
||||
|
||||
### Monatliches Reset
|
||||
|
||||
```typescript
|
||||
// Cron Job oder in API Route
|
||||
if (new Date().getDate() === 1) {
|
||||
await pb.collection('users').update(userId, { links_count: 0 });
|
||||
}
|
||||
```
|
||||
|
||||
### Customer Portal
|
||||
|
||||
```typescript
|
||||
// Abo verwalten
|
||||
const portal = await stripe.billingPortal.sessions.create({
|
||||
customer: user.stripe_customer_id,
|
||||
return_url: `${PUBLIC_APP_URL}/account`
|
||||
});
|
||||
```
|
||||
|
||||
## Probleme?
|
||||
|
||||
### "Invalid Stripe API version"
|
||||
|
||||
→ Verwende `apiVersion: '2024-10-28.acacia'` beim Stripe initialisieren
|
||||
|
||||
### "Price ID not found"
|
||||
|
||||
→ Alle Umgebungsvariablen setzen:
|
||||
|
||||
- STRIPE_PRICE_MONTHLY
|
||||
- STRIPE_PRICE_YEARLY
|
||||
- STRIPE_PRICE_LIFETIME
|
||||
|
||||
### "No such price"
|
||||
|
||||
→ Price ID aus Stripe Dashboard kopieren
|
||||
|
||||
### Webhook funktioniert nicht
|
||||
|
||||
→ `stripe listen` läuft? Terminal Output prüfen
|
||||
|
||||
### User wird nicht auf Pro gesetzt
|
||||
|
||||
→ PocketBase Permissions prüfen
|
||||
|
||||
## Das war's! 🚀
|
||||
|
||||
Von 0 auf bezahlte Subscriptions in einer Stunde. Keine Magie, nur die absolut nötigsten Teile.
|
||||
29
apps/uload/docs/stripe/deployment-setup.md
Normal file
29
apps/uload/docs/stripe/deployment-setup.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Stripe Deployment Setup
|
||||
|
||||
## Required Environment Variables
|
||||
|
||||
Add these to your deployment platform (e.g., Coolify):
|
||||
|
||||
```bash
|
||||
# Stripe API Keys (Test)
|
||||
PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51RujJlPRtmsJbOMgKRJu4uOqOGzGXwI8FT0qwf1jJUQ0HJIDmxBR3fzJSqGhVQCJ5xAJ4jKh0U6JvfLdx76FpMGB00xQI2j4qg
|
||||
|
||||
# Stripe Product Configuration
|
||||
STRIPE_PRODUCT_ID=prod_SrqNlCbfaaKSnk
|
||||
STRIPE_PRICE_MONTHLY=price_1Rw6hkPRtmsJbOMgdUYfj7ee
|
||||
STRIPE_PRICE_YEARLY=price_1Rw6j0PRtmsJbOMgTGrOZH2c
|
||||
STRIPE_PRICE_LIFETIME=price_1Rw6qPPRtmsJbOMgsS6nnBTM
|
||||
```
|
||||
|
||||
## Finding Your Values
|
||||
|
||||
1. **API Keys**: Stripe Dashboard → Developers → API Keys
|
||||
2. **Product ID**: Stripe Dashboard → Products → Select product → Copy ID
|
||||
3. **Price IDs**: Stripe Dashboard → Products → Select product → Pricing section → Copy price IDs
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Use test keys (`sk_test_`, `pk_test_`) for development
|
||||
- Use live keys (`sk_live_`, `pk_live_`) for production
|
||||
- All variables must be set or checkout will fail with "Price ID not found"
|
||||
- Restart application after adding variables
|
||||
195
apps/uload/docs/stripe/testing-checklist.md
Normal file
195
apps/uload/docs/stripe/testing-checklist.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# Stripe Subscription Testing Checklist
|
||||
|
||||
## 🔧 Setup für Tests
|
||||
|
||||
### 1. Test-Umgebung
|
||||
|
||||
- [x] Dev Server läuft (`npm run dev`)
|
||||
- [ ] Test-Keys sind eingetragen (nicht Live-Keys!)
|
||||
- [ ] Stripe CLI installiert: `brew install stripe/stripe-cli/stripe`
|
||||
- [ ] Webhook listening: `stripe listen --forward-to localhost:5173/api/stripe/webhook`
|
||||
|
||||
### 2. Test User anlegen
|
||||
|
||||
```bash
|
||||
# In PocketBase Admin (http://localhost:8090/_/)
|
||||
1. Users Collection öffnen
|
||||
2. Neuen User erstellen:
|
||||
- Email: test@example.com
|
||||
- Password: testtest
|
||||
- subscription_status: "free"
|
||||
- links_count: 0
|
||||
```
|
||||
|
||||
## 💳 1. Checkout Flow testen
|
||||
|
||||
### Monthly Subscription
|
||||
|
||||
- [ ] Auf `/pricing` gehen
|
||||
- [ ] "Monthly" Button klicken
|
||||
- [ ] Stripe Checkout öffnet sich
|
||||
- [ ] Korrekter Preis: €4.99/month
|
||||
- [ ] Test-Karte: `4242 4242 4242 4242`
|
||||
- [ ] Payment erfolgreich
|
||||
|
||||
### Yearly Subscription
|
||||
|
||||
- [ ] "Yearly" Button testen
|
||||
- [ ] Korrekter Preis: €39.99/year
|
||||
- [ ] Payment erfolgreich
|
||||
|
||||
### Lifetime Payment
|
||||
|
||||
- [ ] "Lifetime" Button testen
|
||||
- [ ] Korrekter Preis: €129.99 (one-time)
|
||||
- [ ] Payment erfolgreich
|
||||
|
||||
## 🔗 2. Webhook Testing
|
||||
|
||||
### Manuell mit Stripe CLI
|
||||
|
||||
```bash
|
||||
# Webhook Event simulieren
|
||||
stripe trigger checkout.session.completed
|
||||
|
||||
# Logs checken:
|
||||
# 1. Terminal wo stripe listen läuft
|
||||
# 2. Browser Konsole
|
||||
# 3. PocketBase für User-Update
|
||||
```
|
||||
|
||||
### Automatisch nach echtem Payment
|
||||
|
||||
- [ ] Payment durchführen
|
||||
- [ ] Webhook wird automatisch ausgelöst
|
||||
- [ ] User Status wird auf "pro" gesetzt
|
||||
- [ ] stripe_customer_id wird gespeichert
|
||||
|
||||
## 👤 3. User Status Testing
|
||||
|
||||
### Free User Limits
|
||||
|
||||
```bash
|
||||
# Als Free User einloggen
|
||||
# Versuche 11 Links zu erstellen
|
||||
```
|
||||
|
||||
- [ ] Link 1-10: Erfolgreich erstellt
|
||||
- [ ] Link 11: Error "Limit erreicht"
|
||||
- [ ] Upgrade-Button wird angezeigt
|
||||
|
||||
### Pro User Unlimited
|
||||
|
||||
- [ ] User auf "pro" setzen (manuell in PocketBase)
|
||||
- [ ] Beliebig viele Links erstellen können
|
||||
- [ ] Kein Limit-Check
|
||||
|
||||
## 🎛️ 4. Subscription Management
|
||||
|
||||
### Status Check API
|
||||
|
||||
```bash
|
||||
curl http://localhost:5173/api/user/subscription
|
||||
```
|
||||
|
||||
- [ ] Gibt aktuellen Status zurück
|
||||
- [ ] Zeigt nächste Zahlung an (falls recurring)
|
||||
|
||||
### Cancel Subscription
|
||||
|
||||
- [ ] Stripe Customer Portal Link funktioniert
|
||||
- [ ] Subscription kann gekündigt werden
|
||||
- [ ] User Status wird entsprechend geupdatet
|
||||
|
||||
## 🧪 5. Edge Cases testen
|
||||
|
||||
### Doppelte Subscription
|
||||
|
||||
- [ ] Pro User versucht erneut zu upgraden
|
||||
- [ ] Error: "Du hast bereits ein aktives Abo"
|
||||
|
||||
### Abgebrochene Payments
|
||||
|
||||
- [ ] Payment abbrechen im Stripe Checkout
|
||||
- [ ] User bleibt auf "free" Status
|
||||
- [ ] Keine Änderungen in PocketBase
|
||||
|
||||
### Webhook Failures
|
||||
|
||||
- [ ] Webhook URL temporär down
|
||||
- [ ] Stripe retries automatisch
|
||||
- [ ] Manual retry über Stripe Dashboard
|
||||
|
||||
## 📊 6. Integration Testing
|
||||
|
||||
### Frontend Updates
|
||||
|
||||
- [ ] Navigation zeigt Pro-Status
|
||||
- [ ] Upgrade-Buttons verschwinden für Pro User
|
||||
- [ ] Richtige Limits werden angezeigt
|
||||
|
||||
### API Protection
|
||||
|
||||
```bash
|
||||
# Als Free User mit 10 Links
|
||||
curl -X POST http://localhost:5173/api/links \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url": "https://example.com"}'
|
||||
```
|
||||
|
||||
- [ ] 403 Error bei Limit überschritten
|
||||
- [ ] Pro User kann unbegrenzt erstellen
|
||||
|
||||
## 🎯 7. Test Cards für verschiedene Szenarien
|
||||
|
||||
### Erfolgreiche Payments
|
||||
|
||||
- `4242 4242 4242 4242` - Visa
|
||||
- `5555 5555 5555 4444` - Mastercard
|
||||
- `4000 0025 0000 3155` - Visa (requires authentication)
|
||||
|
||||
### Failed Payments
|
||||
|
||||
- `4000 0000 0000 0002` - Card declined
|
||||
- `4000 0000 0000 9995` - Insufficient funds
|
||||
|
||||
### SEPA Testing
|
||||
|
||||
- IBAN: `DE89370400440532013000`
|
||||
- [ ] SEPA Direct Debit funktioniert
|
||||
|
||||
## ✅ Completion Checklist
|
||||
|
||||
- [ ] Alle Checkout-Flows funktionieren
|
||||
- [ ] Webhooks verarbeiten Events korrekt
|
||||
- [ ] User Status Updates funktionieren
|
||||
- [ ] Link-Limits werden enforced
|
||||
- [ ] Pro Features sind freigeschaltet
|
||||
- [ ] Error Handling funktioniert
|
||||
- [ ] Frontend zeigt korrekten Status
|
||||
|
||||
## 🚨 Häufige Probleme
|
||||
|
||||
### Webhook nicht erhalten
|
||||
|
||||
```bash
|
||||
# Check Stripe CLI Output
|
||||
stripe listen --forward-to localhost:5173/api/stripe/webhook --log-level debug
|
||||
```
|
||||
|
||||
### User Status nicht geupdated
|
||||
|
||||
```sql
|
||||
-- In PocketBase Admin Console
|
||||
SELECT * FROM users WHERE email = 'test@example.com';
|
||||
```
|
||||
|
||||
### Payment fehlgeschlagen
|
||||
|
||||
- API Keys überprüfen (Test vs Live)
|
||||
- Price IDs korrekt?
|
||||
- Webhook Endpoint erreichbar?
|
||||
|
||||
---
|
||||
|
||||
**Tipp**: Teste immer mit Test-Daten und Test-Keys. Niemals echte Payments in der Entwicklung!
|
||||
Loading…
Add table
Add a link
Reference in a new issue