chahinebrini 22385d7d67 feat(stripe,onboarding): tier-rename + TTS audio button in lyra bubble
## Stripe Checkout Rename

Alte Legacy-Tier-Namen 'standard/pro' (von alter Tier-Struktur) waren
irreführend — heute heißt es 'pro/legend'. Cleanup:

- ENV-Var-Namen: STRIPE_PRICE_<PLAN>_<BILLING> (computed) statt
  hardcoded STANDARD/PRO Mapping. Erwartet:
    STRIPE_PRICE_PRO_MONTHLY
    STRIPE_PRICE_PRO_YEARLY
    STRIPE_PRICE_LEGEND_MONTHLY
    STRIPE_PRICE_LEGEND_YEARLY
- 'quarterly' billing entfernt (Strategist-Verdict: nur monthly + yearly,
  '2 Monate gratis' bei yearly).
- metadata enthält jetzt billing zusätzlich zu plan.

Webhook-Audit: bereits korrekt (mapped session.metadata.plan → pro/legend/free
via simple switch).

User-Action benötigt (Stripe Test-Dashboard):
- 4 Products + Prices anlegen mit 14-Tage-Trial
- Pricing pro Strategist: Pro 3,99/Mo + 39,90/Yr (2mo gratis),
  Legend 7,99/Mo + 79,90/Yr
- Webhook-Endpoint: https://staging.rebreak.org/api/stripe/webhook
  (Events: checkout.session.completed, customer.subscription.{updated,deleted})
- ENV-Vars (incl. STRIPE_WEBHOOK_SECRET) in Infisical pflegen

## TTS Audio-Button in LyraBubble

DiGA-Accessibility: Screen-Reader-Alternative + Lese-Hürden-Mitigation.

- lib/lyraSpeech.ts: one-shot TTS-Helper (vereinfacht aus SosTtsQueue)
  - Fetch /api/coach/speak mit Auth-Token
  - Bytes → Base64 → temp-file → expo-av Audio.Sound
  - Stop-fn: abortet in-flight fetch + unloaded sound
  - Status-callback: idle | loading | playing
- LyraBubble: Audio-Button rechts oben (orange Pill, 34×34)
  - Icon: volume-medium / hourglass / stop je nach status
  - Auto-stop bei text-change (Slide-Switch) + unmount
  - A11y-Labels in 4 Sprachen (audio_play / audio_loading / audio_stop)

Bubble-paddingRight erhöht auf 50 für Button-Platz.

## Locales

de/en/fr/ar: onboarding.lyra.audio_play / audio_loading / audio_stop

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:51:11 +02:00

86 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Stripe from "stripe";
/**
* POST /api/stripe/checkout
*
* Erstellt eine Stripe-Checkout-Session für Pro oder Legend Subscription.
* Frontend (Web) öffnet die zurückgegebene URL — User landet im Stripe-Hosted
* Checkout, gibt Zahlungsmethode ein, wird zu success_url umgeleitet.
*
* iOS muss via RevenueCat/Apple-IAP — Stripe ist Web-only-Path (Apple-Guideline
* 3.1.1 verbietet externe Bezahlwege für digitale Subs in iOS-Apps).
*
* Body: { plan: 'pro' | 'legend', billing: 'monthly' | 'yearly' }
*
* ENV-Vars (in Infisical):
* STRIPE_PRICE_PRO_MONTHLY — Test/Live Stripe price_xxx ID
* STRIPE_PRICE_PRO_YEARLY
* STRIPE_PRICE_LEGEND_MONTHLY
* STRIPE_PRICE_LEGEND_YEARLY
*
* Tier-Mapping (post Free-Drop Pivot, 2026-05):
* Pro → 3,99 €/mo · 39,90 €/yr (2 Monate gratis)
* Legend → 7,99 €/mo · 79,90 €/yr (Multi-Device-Hardening)
*
* 14-Tage-Trial wird im Stripe-Dashboard pro Price konfiguriert (Trial-Period-
* Days) — der Endpoint passt sich automatisch an wenn der Price die Trial hat.
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
if (!config.stripeSecretKey) {
throw createError({
statusCode: 500,
message: "Stripe nicht konfiguriert STRIPE_SECRET_KEY fehlt",
});
}
const stripe = new Stripe(config.stripeSecretKey);
const user = await requireUser(event);
const body = await readBody(event);
const plan = body?.plan as string;
const billing = (body?.billing as string) || "monthly";
if (!plan || !["pro", "legend"].includes(plan)) {
throw createError({
statusCode: 400,
message: "Ungültiger Plan (erwartet: 'pro' oder 'legend')",
});
}
if (!["monthly", "yearly"].includes(billing)) {
throw createError({
statusCode: 400,
message: "Ungültiger Billing-Zyklus (erwartet: 'monthly' oder 'yearly')",
});
}
// ENV-Var-Naming-Pattern: STRIPE_PRICE_<PLAN>_<BILLING> in Großbuchstaben
const envKey = `STRIPE_PRICE_${plan.toUpperCase()}_${billing.toUpperCase()}`;
const priceId = process.env[envKey];
if (!priceId || !priceId.startsWith("price_")) {
throw createError({
statusCode: 503,
message: `Dieser Plan ist noch nicht verfügbar. (${envKey} nicht in Infisical gesetzt)`,
});
}
const appUrl = config.public.appUrl || "https://rebreak.org";
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${appUrl}/app/settings?upgraded=true`,
cancel_url: `${appUrl}/pricing`,
client_reference_id: user.id,
metadata: {
user_id: user.id,
plan,
billing,
},
});
return { url: session.url };
});