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>
This commit is contained in:
Timo
2026-08-10 12:50:12 +02:00
parent 5da8027b68
commit e3a28b0a1c
9 changed files with 800 additions and 161 deletions

View File

@@ -0,0 +1,320 @@
# 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.