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:
320
docs/superpowers/plans/2026-08-09-weekly-subscription-plan.md
Normal file
320
docs/superpowers/plans/2026-08-09-weekly-subscription-plan.md
Normal 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 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.
|
||||
Reference in New Issue
Block a user