# 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 3–4-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 1–3 — 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. D1–D4 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.