Files
Greenlens/docs/superpowers/plans/2026-08-09-weekly-subscription-plan.md
Timo e3a28b0a1c feat(billing): add weekly_pro subscription plan
Adds a 2.99 EUR/week plan with a 3-day free trial alongside the existing
monthly and yearly subscriptions.

Backend: weekly_pro joins the supported subscription products, the
available product list and the Discord sales label. No schema change --
weekly Pro grants the same 100 credits per calendar month as monthly Pro,
so no column is needed to tell the two apart.

Paywall: weekly and yearly are the two prominent cards, monthly is a
selectable row below them. Weekly is preselected. Cards only render when
their RevenueCat package exists, and the selection falls back to a
visible plan so the CTA can never buy a product that is not loaded.

Trial eligibility: checkTrialOrIntroductoryPriceEligibility now gates the
trial copy. Apple grants one intro offer per subscription group, so with
two trial products a second free-trial promise would otherwise be shown
to users who get charged immediately. Anything but a clear ELIGIBLE is
treated as no trial, as the RevenueCat SDK recommends.

Analytics: trial_started previously fired on every subscription purchase,
including monthly which never had a trial. It now fires only for products
that actually carry one. paywall_viewed distinguishes trial_enabled from
trial_eligible and reports selected_plan.

Tests: 9 new cases covering the entitlement path, credits, renewal period
and trial allowance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:50:38 +02:00

321 lines
20 KiB
Markdown
Raw Permalink 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.
# Plan: Wochen-Abo (`weekly_pro`) als dritte Abo-Auswahl
Stand: 2026-08-09. Ziel: neben `monthly_pro` (4,99 €/Monat) und `yearly_pro` (39,99 €/Jahr) ein
Wochen-Abo als Einstiegsoption auf der Paywall.
---
## 0. Entscheidungen vorab (blockieren alles andere)
| # | Frage | Status | Konsequenz wenn anders |
|---|-------|--------|------------------------|
| D1 | Preis? | **ENTSCHIEDEN: 2,99 €/Woche** (≈ 12,95 €/Monat, bewusst teurer pro Monat als das Monatsabo) | — |
| D2 | Credits? | **ENTSCHIEDEN: gleich wie Pro, 100/Monatszyklus** (= Option A, kein Schema-Change) | — |
| D3 | Trial? | **ENTSCHIEDEN: 3 Tage gratis** auf weekly (7 Tage wären bei einem Wochenprodukt eine komplette Gratisperiode) | — |
| D4 | Android? | OFFEN — nur wenn Play-Release aktiv ist | Sonst Play-Console-Schritte streichen |
> **D3 zieht Arbeit nach sich, die es ohne Trial nicht gäbe:** siehe Abschnitt 5.4
> (Trial-Eligibility). Das ist nach dem Paywall-Umbau der zweitgrößte Posten im Plan.
### Warum D2 so wichtig ist
Der Credit-Zyklus im Backend ist **hart auf den UTC-Kalendermonat verdrahtet**
([billing.js:44-62](server/lib/billing.js:44)) und `billing_accounts` hat **keine Spalte, die
Monats- von Wochen-Pro unterscheidet** — nur `plan = 'free' | 'pro'`
([billing.js:815-826](server/lib/billing.js:815)).
- **Option A (empfohlen, D2 = 100 Credits):** kein Schema-Change, kein Migrations-Risiko.
Backend-Aufwand ≈ 4 Zeilen.
- **Option B (eigenes Wochen-Kontingent):** braucht (1) neue Spalte `plan_tier`/`product_id`
inkl. `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` in `ensureBillingSchema`, (2) `getCycleBounds`
muss vom Account abhängen statt global Kalendermonat zu sein, (3) `alignAccountToCurrentCycle`
und `isAllowedMonthlyAllowance` müssen den neuen Tier kennen, sonst wird die Allowance beim
nächsten Request **stillschweigend auf 100 zurückgesetzt** ([billing.js:147-168](server/lib/billing.js:147)).
Rechne mit dem 34-fachen Aufwand.
> **Bekannte Kante bei Option A:** Wer am 30. eines Monats eine Woche kauft, bekommt am 1. den
> nächsten 100er-Block — also 200 Credits für eine Woche Zahlung. Der gleiche Effekt existiert
> heute schon beim Monatsabo, ist beim Wochenabo aber ausgeprägter. Bewusst akzeptieren oder
> Option B wählen.
---
## Fortschritt (Stand 2026-08-10)
**Erledigt im Code** (`tsc --noEmit` sauber, App-Tests 121 grün, Server-Tests 26 grün):
- Abschnitt 4 komplett — `billing.js` (3 Konstanten) + `discord.js` Label.
- Abschnitt 5.1 komplett — `contracts.ts`, `AppContext.tsx` (beide Stellen).
- Abschnitt 6, Mock-Teil — `mockBackendService.ts`; `simulatePurchase` nutzt jetzt das
Produkt-Set statt hartkodierter Vergleiche, plus `RENEWAL_DAYS_BY_SUBSCRIPTION` (weekly = 7 Tage).
- Abschnitt 5.2, Plumbing — `SubscriptionProductId`, neuer `isSubscriptionProductId()`-Helper
(ersetzt alle drei hartkodierten `monthly_pro || yearly_pro`-Vergleiche),
`resolveSubscriptionPackages` löst `weekly_pro` über `PACKAGE_TYPE.WEEKLY` auf,
Package-Auswahl beim Kauf über `subscriptionPackages[productId]`.
- Abschnitt 5.2, UI — **Layout-Entscheidung: Weekly + Yearly prominent, Monthly als eigene
anklickbare Zeile darunter.** Zwei Karten mit Radio-Indikator, ein CTA darunter.
**Reihenfolge Weekly → Yearly, Vorauswahl = Weekly.** Weekly-Karte rendert nur, wenn das
RevenueCat-Package existiert (`renderPlanCard`), sonst fällt sie ersatzlos weg — mit
Fallback auf Yearly, damit die Vorauswahl nie auf einen unsichtbaren Plan zeigt.
- Abschnitt 5.3 — Copy in de/es/en: `planWeeklyName`, `perWeek`, `proWeeklyPlanPriceBare`,
`trialReminderWeekly` (1 Tag statt 2, weil der Trial nur 3 Tage läuft).
- Abschnitt 7 — `selected_plan` liegt jetzt an `paywall_viewed` an.
- **Monatsäquivalent bleibt dem Jahresabo vorbehalten** (Entscheidung vom 2026-08-10).
`toMonthlyEquivalent(price, periodsPerYear)` ersetzt die alte Jahres-Sonderlogik, wird aber
nur für Yearly aufgerufen. Beim Wochenabo steht ausschließlich „2,99 € / Woche".
Bewusst asymmetrisch: die Umrechnung erscheint dort, wo sie den Preis kleiner wirken lässt.
Rechtlich unkritisch (Preis + Periode sind klar ausgewiesen, mehr verlangt weder Apple noch
die PAngV), aber es ist die Stelle, an der ein genauer Leser Absicht erkennen kann —
relevant, falls Refund-Quote oder Review-Tonfall später auffällig werden.
- Abschnitt 5.4 komplett — `checkTrialOrIntroductoryPriceEligibility(['weekly_pro','yearly_pro'])`
läuft im selben `Promise.all` wie Offerings und Top-ups (eigener `catch`, damit ein Ausfall
der Prüfung nicht den Store-Load mitreißt). Nur ein klares `ELIGIBLE` gilt als berechtigt —
`UNKNOWN` wird bewusst als „kein Trial" behandelt, so wie es die SDK-Doku empfiehlt.
Nicht berechtigt → die Karte zeigt nur `planWeeklyNamePlain` / `planYearlyNamePlain`,
kein `dueSummary`-Banner, CTA = „Jetzt starten", keine Trial-Erinnerung im Footer.
- `trial_started` feuerte bisher bei **jedem** Abo-Kauf, auch bei Monatlich ohne Testphase.
Feuert jetzt nur noch, wenn das gekaufte Produkt tatsächlich eine Testphase hatte.
`paywall_viewed` unterscheidet zusätzlich `trial_enabled` (Plan sieht Trial vor) von
`trial_eligible` (Apple gewährt ihn diesem Nutzer).
- Abschnitt 6, Tests — 9 neue Testfälle. Server (`server/test/billing.test.js`): `weekly_pro`
steht in `AVAILABLE_PRODUCTS`, trägt ein gültiges Pro-Entitlement, Monthly/Yearly weiterhin
auch, ein Top-up-Produkt weiterhin **nicht**, und die Allowance liegt bei 100 bzw. 30 im Trial.
Client (`__tests__/services/mockBackendService.test.ts`): Kauf setzt Pro mit 100 Credits und
`renewsAt` +7 Tage, `weekly_pro` ist in `availableProducts`, RevenueCat-Sync erkennt ein
`weekly_pro`-Abo, und ein `weekly_pro`-Trial bekommt nur 30 Credits.
Gegenprobe gemacht: mit zurückgedrehtem `SUPPORTED_SUBSCRIPTION_PRODUCTS` schlägt der
Entitlement-Test fehl — der Test bewacht also tatsächlich die Regression.
**Offen — alles außerhalb des Codes:**
- Abschnitte 13 — Store- und RevenueCat-Setup (nur du).
- Abschnitt 9 — Sandbox-Verifikation, insbesondere die drei Trial-Szenarien.
- Optional: das „Abo verwalten"-Modal für bestehende Pro-Nutzer listet weiterhin nur
Free / Pro monatlich / Pro jährlich. Unkritisch, weil dort jeder Tap ohnehin in die
iOS-Einstellungen führt.
- Kosmetik: `offerCardOuter`, `offerBadge`, `offerAltLink` & Co. sind jetzt ungenutzte Styles.
**Hinweis zum Diff:** `services/backend/mockBackendService.ts` hatte gemischte Zeilenenden;
mein Schreibvorgang hat die Datei einheitlich normalisiert. Dadurch zeigt `git diff` dort ~90
zusätzliche Zeilen, die inhaltlich identisch sind — `git diff --ignore-all-space` weist genau
die beabsichtigten 12 Einfügungen / 4 Löschungen aus. Alle anderen Dateien sind unauffällig.
---
## 1. App Store Connect (Apple)
- [ ] **Neues Abo im BESTEHENDEN Subscription Group** anlegen (nicht in einer neuen Gruppe —
sonst kein Up-/Downgrade zwischen den Plänen und Apple behandelt sie als parallele Abos).
- [ ] Produkt-ID exakt `weekly_pro` (gleiche Konvention wie `monthly_pro`/`yearly_pro`; der
Client matcht auf `pkg.product.identifier === productId`, [billing.tsx:46](app/profile/billing.tsx:46)).
- [ ] Laufzeit: **1 Woche**.
- [ ] Preis: **2,99 €**, Preisplan für alle Territorien setzen.
- [ ] **Group Ranking / Level:** weekly = niedrigstes Level (unter monthly, unter yearly), damit
Upgrades sofort und Downgrades zum Periodenende greifen.
- [ ] Lokalisierungen: Anzeigename + Beschreibung für **de, es, en**.
- [ ] Review-Screenshot der Paywall hochladen (Apple lehnt sonst ab).
- [ ] **Introductory Offer: 3 Tage kostenlos** auf `weekly_pro` anlegen (Typ „Free Trial").
Beim Anlegen prüfen, welche Laufzeiten das Dropdown für ein Wochenprodukt überhaupt
anbietet — Apple schränkt die Trial-Dauer relativ zur Abo-Laufzeit ein.
- [ ] ⚠️ **Eligibility-Kollision bewusst machen:** Intro-Offer-Eligibility gilt **pro
Subscription Group**, nicht pro Produkt. Alle drei Abos liegen in derselben Gruppe.
Wer den 3-Tage-Weekly-Trial nimmt, ist damit für den 7-Tage-Yearly-Trial verbrannt —
und umgekehrt. Das muss die Paywall abbilden, siehe Abschnitt 5.4.
- [ ] Status auf „Ready to Submit". **Neue In-App-Käufe werden nur zusammen mit einem
App-Build reviewt** — d. h. ohne neuen Build bleibt das Produkt hängen.
- [ ] App-Beschreibung prüfen: Apple 3.1.2 verlangt Laufzeit + Preis aller Abos in der
Store-Beschreibung + Links zu Terms/Privacy.
## 2. Google Play (nur bei D4 = ja)
- [ ] Play Console → Monetarisierung → Abos → neues Abo `weekly_pro`.
- [ ] Basisplan `weekly-autorenewing`, Abrechnungszeitraum 1 Woche, Preis, aktivieren.
- [ ] Auf denselben Entitlement-Tag mappen.
## 3. RevenueCat
- [ ] **Products** → neues Produkt `weekly_pro` importieren (App Store, ggf. Play).
- [ ] **Entitlements**`pro``weekly_pro` anhängen.
(Entitlement-ID kommt aus `REVENUECAT_PRO_ENTITLEMENT_ID`, Default `pro`
[billing.js:7](server/lib/billing.js:7), [AppContext.tsx:56](services/backend/mockBackendService.ts:56).)
- [ ] **Offerings** → das `current`-Offering → Package mit Identifier `$rc_weekly` hinzufügen,
Produkt `weekly_pro` zuweisen.
Der Client löst über `PACKAGE_TYPE.WEEKLY` auf, sobald der Code aus Schritt 5 drin ist.
- [ ] Warten bis RC den Store-Sync bestätigt („Product is available"), sonst kommt das Package
nicht in `Purchases.getOfferings()` an.
- [ ] Webhook: **keine Änderung nötig**, `/api/revenuecat/webhook` bleibt
([index.js:550](server/index.js:550)).
## 4. Backend (`server/`)
Minimal bei Option A — aber **alle drei Stellen sind Pflicht**:
- [ ] [billing.js:8](server/lib/billing.js:8) — `SUPPORTED_SUBSCRIPTION_PRODUCTS` um `'weekly_pro'` erweitern.
- [ ] [billing.js:10-16](server/lib/billing.js:10) — `TOPUP_CREDITS_BY_PRODUCT` um `weekly_pro: 0`.
- [ ] [billing.js:18](server/lib/billing.js:18) — `AVAILABLE_PRODUCTS` um `'weekly_pro'`.
- [ ] [discord.js:12-13](server/lib/discord.js:12) — `PRODUCT_LABELS` um `weekly_pro: 'Pro (wöchentlich)'`,
sonst steht im Sales-Channel eine rohe Produkt-ID.
> **Warum Punkt 1 nicht optional ist:** `getValidProEntitlement`
> ([billing.js:332-360](server/lib/billing.js:332)) verwirft ein aktives `pro`-Entitlement, wenn
> die `productIdentifier` nicht in der Menge steht. Ohne den Eintrag bleibt ein Weekly-Käufer
> beim Client-Sync **auf `free`**, bis zufällig der Webhook durchläuft (der Webhook-Pfad greift
> über `entitlement_ids` und würde funktionieren — der direkte Sync nach dem Kauf aber nicht).
**Trial-Credits prüfen (wegen D3):** `TRIAL_MONTHLY_CREDITS = 30`
([billing.js:4](server/lib/billing.js:4)) wird produktunabhängig vergeben, sobald RevenueCat
`period_type = trial` meldet ([billing.js:72](server/lib/billing.js:72),
[billing.js:367](server/lib/billing.js:367)). Ein 3-Tage-Weekly-Trial gibt damit **30 Credits für
den ganzen Kalendermonat** — mehr, als der Trial wirtschaftlich hergibt. Entweder bewusst
akzeptieren (einfach) oder die Trial-Allowance produktabhängig machen (braucht dieselbe
Tier-Spalte wie Option B).
Bei Option B zusätzlich: neue Konstante `WEEKLY_PRO_CREDITS`, `getMonthlyAllowanceForPlan`
([billing.js:64](server/lib/billing.js:64)), `isAllowedMonthlyAllowance`
([billing.js:76](server/lib/billing.js:76)), `getCycleBounds`, Spalte + Migration in
`ensureBillingSchema` ([billing.js:812](server/lib/billing.js:812)).
## 5. Mobile App
### 5.1 Typen & Sync
- [ ] [contracts.ts:5](services/backend/contracts.ts:5) — `PurchaseProductId` um `'weekly_pro'`.
- [ ] [AppContext.tsx:90](context/AppContext.tsx:90) — `SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS`
um `'weekly_pro'` (sonst greift die lokale Entitlement-Anwendung nach dem Kauf nicht,
[AppContext.tsx:120-124](context/AppContext.tsx:120)).
- [ ] [AppContext.tsx:466](context/AppContext.tsx:466) — `availableProducts`-Liste.
### 5.2 Paywall (`app/profile/billing.tsx`)
- [ ] [:23](app/profile/billing.tsx:23) `SubscriptionProductId` um `'weekly_pro'`.
- [ ] [:27](app/profile/billing.tsx:27) `PaywallPlanId` um `'weekly'`.
- [ ] [:52-67](app/profile/billing.tsx:52) `resolveSubscriptionPackages``weekly_pro` über
`PACKAGE_TYPE.WEEKLY` auflösen; `offering.weekly` in die Kandidatenliste aufnehmen.
- [ ] [:480](app/profile/billing.tsx:480) Warn-Check um das Weekly-Package erweitern.
- [ ] [:444](app/profile/billing.tsx:444) Default-Auswahl festlegen (Empfehlung: `'monthly'` lassen).
- [ ] [:531-536](app/profile/billing.tsx:531) `weeklyPackage` + `weeklyPrice` (Fallback-Copy).
- [ ] [:646](app/profile/billing.tsx:646) Package-Auswahl von Ternary auf Map umstellen.
- [ ] **Drei Stellen mit hartkodiertem `productId === 'monthly_pro' || productId === 'yearly_pro'`**
([:598](app/profile/billing.tsx:598), [:629](app/profile/billing.tsx:629),
[:640](app/profile/billing.tsx:640)) durch einen `isSubscriptionProductId()`-Helper ersetzen —
sonst läuft ein Weekly-Kauf in den Top-up-Zweig.
- [ ] [:1130](app/profile/billing.tsx:1130) Verwaltungs-Modal (`handlePurchase('monthly_pro')`) prüfen.
- [ ] **UI-Umbau:** Die Paywall zeigt aktuell *eine* Angebotskarte + einen Textlink zur
Alternative ([:875-884](app/profile/billing.tsx:875)). Das ist ein binäres Muster und
trägt keine dritte Option. Ersetzen durch eine kompakte 3-Zeilen-Plan-Auswahl
(weekly / monthly / yearly) über dem CTA. Das ist der größte Einzelposten im ganzen Plan.
- [ ] `yearlyMonthlyEquivalent` ([:747](app/profile/billing.tsx:747)) analog für weekly →
Monatsäquivalent, damit der Preisvergleich ehrlich bleibt.
### 5.3 Copy — **dreimal**, für de / es / en in `getBillingCopy` ([:103-420](app/profile/billing.tsx:103))
Neue Keys: `planWeeklyName`, `perWeek`, `proWeeklyPlanPriceBare` (`2,99 €`), `autoRenewWeekly`,
`altWeekly(price)`, `planCardPriceWeekly(price)`. Pflichtangabe im Text: automatische
wöchentliche Verlängerung + Kündigungsweg (steht heute in `autoRenewMonthly`/`autoRenewYearly`).
Wegen D3 zusätzlich: `planWeeklyNameTrial` („Wöchentlich 3 Tage gratis"),
`ctaTrialWeekly`, `dueSummaryWeekly`, `trialReminderWeekly` (die 2-Tage-Vorwarnung aus
`trialReminder` ist bei 3 Tagen Trial knapp — Formulierung anpassen oder auf 1 Tag ziehen),
plus je eine **Nicht-berechtigt-Variante** der Trial-Texte (siehe 5.4).
### 5.4 Trial-Eligibility — neu, weil D3 = Trial
Heute hat nur `yearly_pro` einen Trial, deshalb zeigt die Paywall die Trial-Texte
**bedingungslos**: `planYearlyName` („Jährlich - 7 Tage gratis"), das `dueSummary`-Banner
(„Heute fällig: 0,00 € - dann 39,99 € am …", [billing.tsx:887-894](app/profile/billing.tsx:887))
und `ctaTrial`. **Es gibt keinerlei Eligibility-Prüfung in der Codebase** (verifiziert).
Mit zwei Trial-Produkten in einer Subscription Group wird das falsch: Nutzer, die ihren
Group-Trial schon verbraucht haben, sehen weiterhin „0,00 € heute" und werden sofort abgebucht.
Das ist ein Refund-/Chargeback-Treiber **und** ein Ablehnungsrisiko nach App-Store-Richtlinie
3.1.2 (irreführende Preisdarstellung).
- [ ] Beim Laden der Offerings ([billing.tsx:463-509](app/profile/billing.tsx:463))
`Purchases.checkTrialOrIntroductoryPriceEligibility(['weekly_pro', 'yearly_pro'])`
mitziehen und das Ergebnis in den State legen.
- [ ] Trial-Copy pro Plan an die Eligibility hängen: bei „nicht berechtigt" fällt die Karte auf
reine Preisdarstellung zurück (kein „gratis", kein `dueSummary`-Banner, CTA = `ctaMonthly`-Variante).
- [ ] Ladezustand abfangen: solange die Eligibility unbekannt ist, **keine** Trial-Zusage rendern
(lieber kurz neutral als kurz falsch).
- [ ] Analytics: Eligibility als Property an `paywall_viewed` hängen — sonst sind die
Conversion-Zahlen zwischen „hat Trial gesehen" und „hat keinen gesehen" vermischt.
Aufwand: ca. ein halber Tag. Ohne diesen Punkt ist D3 nicht releasefähig.
## 6. Mock & Tests
- [ ] [mockBackendService.ts:48-57](services/backend/mockBackendService.ts:48) — beide Konstanten.
- [ ] [mockBackendService.ts:258](services/backend/mockBackendService.ts:258) — `availableProducts`.
- [ ] [mockBackendService.ts:1089](services/backend/mockBackendService.ts:1089) — Abo-Zweig in `simulatePurchase`.
- [ ] `__tests__/services/mockBackendService.test.ts` erweitern.
- [ ] `server/test/billing.test.js`: Test, dass `weekly_pro` das Pro-Entitlement erteilt
(aktuell keine Produkt-IDs in dieser Datei — neuer Test).
- [ ] `npm run test` + `npx tsc --noEmit`.
## 7. Analytics
- [ ] [billing.tsx:511-529](app/profile/billing.tsx:511) — `trial_enabled` ist ein Boolean, abgeleitet
aus `selectedPaywallPlan === 'yearly'`. Mit drei Plänen ist Weekly im Funnel nicht von
Monthly unterscheidbar. **`selected_plan: PaywallPlanId` als Property ergänzen** und die
PostHog-Funnels darauf umstellen.
- [ ] `subscription_started` / `purchase_initiated` tragen bereits `product_id` — ok.
## 8. Rechtliches / Landing
- [ ] [PrivacyContent.tsx](greenlns-landing/app/privacy/PrivacyContent.tsx) und Terms prüfen: Werden
Abo-Laufzeiten dort einzeln aufgezählt? Dann Wochenabo ergänzen.
- [ ] App-Store-Beschreibung (siehe Abschnitt 1, letzter Punkt).
- [ ] Die Landing Page listet aktuell **keine** Preise — nichts zu tun, außer du willst es dort zeigen.
## 9. Build, Test, Release
- [ ] Dev-Build/TestFlight — Expo Go kann keine echten Käufe (`Constants.appOwnership === 'expo'`
→ Simulationspfad, [billing.tsx:88](app/profile/billing.tsx:88)).
- [ ] Sandbox-Tester: Wochenabo kaufen → prüfen dass
(a) Paywall alle drei Preise aus dem Store zieht (nicht die Fallback-Copy),
(b) Plan sofort auf Pro springt (Client-Sync, Abschnitt 4/5.1),
(c) Credits stimmen,
(d) RevenueCat-Dashboard das Event zeigt,
(e) Discord-Sales-Webhook „Pro (wöchentlich)" meldet,
(f) „Käufe wiederherstellen" funktioniert.
- [ ] **Trial-Szenarien mit zwei frischen Sandbox-Accounts** (Eligibility lässt sich pro Account
nicht zurücksetzen — pro Durchlauf ein neuer Tester):
(a) frischer Account → Weekly zeigt „3 Tage gratis", Abbuchung erst an Tag 3;
(b) Account, der den Weekly-Trial verbraucht hat → Yearly zeigt **kein** „7 Tage gratis"
und **kein** „Heute fällig 0,00 €";
(c) umgekehrt: Yearly-Trial verbraucht → Weekly zeigt keinen Trial.
Fällt (b) oder (c) durch, ist der Release blockiert (siehe 5.4).
- [ ] Upgrade weekly → monthly/yearly im Sandbox testen (Group Ranking).
- [ ] Backend deployen (Root-`docker-compose.yml`), **vor** dem App-Release — sonst laufen erste
Käufe gegen ein Backend, das `weekly_pro` nicht kennt.
- [ ] Build & Submit:
`npx eas-cli build:version:set -p ios``npx eas-cli build -p ios --profile production`
`npx eas-cli submit -p ios --latest`
- [ ] Beim Submit die neue Subscription im Review mit einreichen.
---
## Reihenfolge
1. D1D4 entscheiden
2. App Store Connect + RevenueCat (Vorlaufzeit: Store-Sync + Review)
3. Backend (Abschnitt 4) → deployen
4. App (5 + 6 + 7)
5. Sandbox-Verifikation (9)
6. Release
## Größte Stolperfallen
1. **Trial-Eligibility gilt pro Subscription Group.** Zwei Trial-Produkte in einer Gruppe, aber
null Eligibility-Prüfung im Code → die Paywall verspricht „0,00 € heute" an Leute, die sofort
abgebucht werden. Abschnitt 5.4, blockiert den Release.
2. `SUPPORTED_SUBSCRIPTION_PRODUCTS` an **beiden** Stellen (Server + AppContext) — sonst bleibt
der Käufer nach dem Kauf auf `free`.
3. Die drei hartkodierten `monthly_pro || yearly_pro`-Vergleiche in `billing.tsx`.
4. Der binäre Paywall-Toggle trägt keine dritte Option — echte UI-Arbeit, nicht nur ein String.
5. Neue IAPs brauchen einen App-Build im Review.
6. `TRIAL_MONTHLY_CREDITS = 30` gilt auch für den 3-Tage-Weekly-Trial.
7. Bei Option B: `isAllowedMonthlyAllowance` überschreibt abweichende Allowances still.