Skip to content

Mobile-Launch-Briefing (iOS + Android)

Stand: 2026-08-02 · Zielleser: Gründer (Solo-Entwickler) und der Agent, der die Mobile-Session startet Repo: /Users/maxhany/targetshot-app/targetshot · Mobile-Paket: frontend/mobile (Workspace @targetshot/mobile)

Alle mit „verifiziert" markierten Zahlen wurden am 02.08.2026 direkt im Repo geprüft. Wo etwas nicht aus dem Repo belegbar ist, steht das ausdrücklich dabei — bitte nicht überschreiben, sondern klären.


1. Ausgangslage — was wirklich existiert

Die Kernaussage in einem Satz

@targetshot/mobile ist kein Gerüst und kein WebView-Wrapper, sondern eine echte, in der CI grün gehaltene Expo/React-Native-App mit vollständig implementiertem Keycloak-Login — die aber seit dem 22.03.2026 eingefroren ist, während das Web um Challenges, Duelle, Bestenliste, Berichte und den Pro-Checkout weitergewachsen ist. Die Aufgabe heißt nicht „App bauen", sondern „App entfrosten, store-fähig machen, Feature-Delta nachziehen".

Wo das Paket liegt (häufigster Irrtum)

frontend/mobilenicht frontend/apps/mobile. Die Workspace-Globs in frontend/package.json sind apps/*, packages/* und explizit mobile. Jeder Pfad-Reflex „apps/mobile" geht ins Leere.

Verifizierter Bestand

Fakt Wert Beleg
Umfang Mobile 10.492 Zeilen in frontend/mobile/src + frontend/mobile/app wc -l über beide Bäume
Umfang Web zum Vergleich 73.296 Zeilen in frontend/apps/web/src dito
Stack Expo SDK ~54, React Native 0.81.5, React 19.1.0, expo-router 6 (typedRoutes) frontend/mobile/package.json
Versionen Mobile-Paket 0.1.0, app.config.ts version: '1.0.0', Web/Root 0.7.0 verifiziert — drei verschiedene Versionsbegriffe, keine Kopplung
Geteilter Code @targetshot/core = 73 Zeilen frontend/packages/core/src/index.ts
Letzte Feature-Arbeit 8e3595d „feat(mobile): make the app release-fit", 2026-03-22 git log -- frontend/mobile
Letzter Commit überhaupt 939889a (2026-05-04) — entfernte den Feedback-Screen, kein Feature dito
Commit-Anteil 11 von 541 Repo-Commits berühren frontend/mobile dito

(Anmerkung für den Agenten: eine kursierende Zahl von „~5.600 LOC" und „packages/core = 95 Zeilen" ist falsch. Die verifizierten Werte stehen oben.)

Was fertig gebaut ist

  • Keycloak-Login, produktionsreif im Flow: OIDC Authorization Code + PKCE über den System-Browser (AuthRequest mit usePKCE: truepromptAsyncexchangeCodeAsync mit code_verifier), Discovery per fetchDiscoveryAsync, Refresh mit 60 s Skew, Session-Restore beim Start. frontend/mobile/src/auth/context.tsx. Damit ist RFC 8252 eingehalten (kein In-App-WebView).
  • Token-Persistenz in expo-secure-store mit eigenem Chunking wegen des 2048-Byte-Limits: src/auth/storage.ts, src/utils/secureStoreChunks.ts.
  • 5 Tabs mit Auth-Guard: Dashboard, Sessions, Pro, Verein, Profil (app/(tabs)/_layout.tsx).
  • Große, echte Screens gegen die Prod-API, keine Mock-Daten: HomeScreen.tsx (1.595 Z., Trendkurve/Serien/Teiler-Top), SessionDetailScreen.tsx (963 Z., echtes SVG-Trefferbild via react-native-svg), TargetHubScreen.tsx (795 Z., Kategorien training/competition/prize), ProfileScreen.tsx (648 Z.).
  • Benachrichtigungs-Inbox gegen denselben Vertrag wie Web (docs/notification-contract.md), inkl. SecureStore-Cache und AppState-Refresh — der reifste Teil der App und der einzige mit CI-Test.
  • Eigenes Mobile-Design-System: src/theme/, GlassSurface, LiquidTabBar, plattformgetrennte Notification-Bells.
  • Fail-closed Runtime-Config: fehlende EXPO_PUBLIC_TARGETSHOT_*-Werte werden hart gemeldet statt still auf Staging zu fallen (src/api/runtimeConfig.ts).
  • CI hängt dran: .github/workflows/ci.yml → „Typecheck mobile frontend" und „Smoke test mobile notifications" (verifiziert, Zeilen 64–74).
  • EAS-Profile: frontend/mobile/eas.json mit development / preview / production (autoIncrement).
  • Doku: frontend/mobile/README.md, docs/mobile-release-checklist.md (in mkdocs.yml verlinkt).

Was fehlt — die harten Blocker

Store-Fähigkeit (verifiziert an frontend/mobile/app.config.ts, 34 Zeilen komplett): - Kein icon, kein splash, kein android.adaptiveIconkein Store-Build einreichbar - Kein extra.eas.projectId, kein ownereas build läuft nicht ohne vorheriges eas init - Kein updates / runtimeVersion → kein OTA - Kein assets/-Verzeichnis, kein ios/, kein android/ (Prebuild nie committet) - eas.json setzt developmentClient: true, aber expo-dev-client fehlt in den Dependencies (verifiziert) - Keine iOS-Permission-Strings in der Config (relevant, sobald Kamera/Fotos/Push dazukommen)

Push existiert nirgends: - expo-notifications fehlt in frontend/mobile/package.json (verifiziert) - Backend hat nur toten Web-Push-Code: backend/src/notifier.ts lädt web-push per try { require }, aber web-push steht nicht in backend/package.json (verifiziert) → sendPush liefert immer { ok: false, reason: 'not_installed' } - Das Prisma-Modell UserPushSubscription ist reines RFC-8291-Web-Push-Schema (endpoint/p256dh/auth) — kein Feld für APNs-/FCM-Device-Token, keine Plattform-Spalte. Nicht wiederverwendbar. - Die Web-App registriert brav einen Service Worker und schickt Subscriptions an /api/notify/subscribe — ins Leere.

Logout ist kaputter als die Doku behauptet (kritisch): README.md und docs/mobile-release-checklist.md sprechen von „logout against the Keycloak end-session endpoint". Der Code ruft in signOut aber ausschließlich zweimal AuthSession.revokeAsync (Zeilen 265 und 275) — kein discovery.endSessionEndpoint, kein id_token_hint (verifiziert per grep). Folge: die Keycloak-SSO-Browser-Session überlebt den Logout; der nächste Login kann denselben Nutzer stillschweigend wieder einloggen. Auf einem geteilten Gerät ist das ein echtes Datenschutzproblem — und die Release-Checkliste lässt sich abhaken, ohne dass es auffällt.

Consent-Gate fehlt vollständig: Der String consent kommt in frontend/mobile/src/** und frontend/mobile/app/** kein einziges Mal vor. Web blockt jede Route bei consent.displayBlocked (apps/web/src/App.tsx:98-101), das Backend antwortet mit 403 (backend/src/services/shootingDataAccess.ts:173). Mobil sieht ein Nutzer ohne bzw. mit widerrufener Einwilligung heute leere Screens und generische Fehler statt der Einwilligungsstrecke.

Feature-Delta zum Web: Die App spricht genau 5 Endpunkte/healthz, /api/profile/me, /api/dashboard/shooting, /api/notifications*, /api/user/settings. Alle bezahlten v0.7.0-Flächen fehlen: Statistiken, Berichte, Bestenliste, Challenges/Duelle, Events, Mitglieder, Vereinsverwaltung, Consent, Jugend.

Zwei von fünf Tabs sind Attrappen: ProScreen.tsx — drei hartkodierte Marketing-Karten mit „Gesperrt"/„Verfügbar" und ein Button „Upgrade folgt in Mobile" ohne onPress (verifiziert, Zeilen 41/58/66). ClubScreen.tsx — Vereinsname + Mannschafts-Chips plus „Nächste Ausbaustufe"-Vorschaukarten.

Weiteres, kleiner aber real: - Kein i18n — alle Strings hart deutsch im JSX - Datenschutz-/AGB-Links zeigen auf https://beta.targetshot.app/privacy bzw. /terms (verifiziert, src/screens/AuthLandingScreen.tsx:99,103) — vor Release auf Prod umstellen - Testabdeckung strukturell blind für UI: vitest.config.ts include ist ['src/**/*.test.ts'] (keine .tsx), environment node, keine RN-Testing-Library → keine einzige der 10.000 UI-Zeilen wird gerendert getestet - Boundary-Check schützt Mobile nicht: frontend/tools/check-package-boundaries.mjs:18 hat SCAN_ROOTS = ['apps','packages']. Passend dazu die Verletzung: src/screens/profile/ProfileChangelogScreen.tsx:2 importiert per '../../../../apps/web/src/data/changelog.json' quer in den Web-Quellbaum. - README.md:107 nennt das Paket noch @app/core, es heißt @targetshot/core - Lokaler Typecheck schlägt fehl (TS7006 in app/(tabs)/_layout.tsx:26), Ursache ist ein unvollständiger lokaler node_modules-Stand (@react-navigation/* fehlt lokal, steht aber im Lockfile). In CI mit frischem npm ci voraussichtlich grün — erst mit sauberem Install gegenprüfen, bevor irgendjemand „Repo-Defekt" ruft.

Nicht aus dem Repo belegbar: Ob der Keycloak-Client targetshot-mobile in Prod tatsächlich existiert. Es gibt keinerlei Infrastruktur-/Deploy-Code, der ihn oder die Redirect-URI anlegt — nur Prosa in README.md und docs/mobile-release-checklist.md. Siehe Abschnitt 8.


2. Die eine große Entscheidung zuerst: Verkauft die App das Abo?

Alles andere hängt daran. TargetShot Pro kostet 5,99 €/Monat bzw. 59 €/Jahr und läuft heute ausschließlich über Stripe im Web (backend/src/routes/personalBilling.ts, apps/web/src/hooks/useProCheckout.ts).

Option A — Kostenlose Companion-App, kein Verkauf in der App

Die App ist ein reiner Zugang zum bezahlten Web-Dienst: einloggen, Daten sehen. Kein Preis, kein Upgrade-Button, kein Link zum Checkout, keine Erwähnung, wo man Pro bekommt.

  • Apple: greift Guideline 3.1.3(f) Free Stand-alone Apps — kostenlose Begleit-App zu einem bezahlten webbasierten Dienst, ausdrücklich ohne IAP-Pflicht, solange in der App weder gekauft noch zum Kauf außerhalb aufgefordert wird. Genau darauf ist in den Review-Notes zu argumentieren, nicht auf 3.1.3(b) Multiplatform Services — dessen Nachsatz („sofern die Inhalte auch als IAP verfügbar sind") lässt sich als IAP-Pflicht lesen und ist für einen Nur-Login-Fall der schwächere Anker. 3.1.3(a) Reader trifft nicht zu (nur Magazine/Bücher/Audio/Video). Die Vereinslizenz lässt sich zusätzlich als 3.1.3(c) Enterprise Services beschreiben (Verkauf an Organisationen), Pro dagegen nicht.
  • Google: gedeckt von der „consumption-only"-Ausnahme der Payments-Policy — Nutzer loggen sich ein und konsumieren anderswo Bezahltes. Android ist hier unkritischer als iOS.
  • Wirtschaftlich: 0 % Store-Provision. Stripe bleibt Zahlungsweg, TargetShot bleibt Vertragspartner und Steuerpflichtiger. Die gesamte deutsche Pflichtstrecke (§ 312j Abs. 3 BGB „zahlungspflichtig bestellen", Widerrufsbelehrung, Bestellbestätigung § 312f Abs. 2, Kündigungsstrecke § 312k) bleibt unverändert im Web und muss nicht dupliziert werden.
  • Kosten: kein Impulskauf aus der App. Außerhalb der US-Storefront darf in der App nicht einmal erklärt werden, wo man Pro bekommt. Upsell läuft über E-Mail und Web — das ist erlaubt (Kommunikation außerhalb der App ist bei beiden Stores frei).
  • Aufwand: ~0 zusätzlicher Code. Im Gegenteil: es muss etwas entfernt werden (siehe unten).

Option B — In-App-Purchase / Play Billing zusätzlich

  • Provision: Apple 30 % Standard, 15 % im App Store Small Business Program (≤ 1 Mio. USD Proceeds im Vorjahr, Anmeldung nötig). Google seit 30.06.2026: 10 % Service Fee + 5 % Billing Fee = 15 % effektiv.
  • Grobrechnung bei 5,99 € brutto: abzüglich 19 % USt ≈ 5,03 € netto; minus 15 % Store ≈ 4,28 €. Über Stripe bleiben schätzungsweise ≈ 4,70 € (Stripe-Gebührensatz nicht aus dem Repo belegt, geschätzt ~1,5 % + 0,25 € für EU-Karten — vor einer Entscheidung im Stripe-Dashboard nachrechnen). Differenz grob 0,40–0,45 € pro Abo und Monat.
  • Aufwand, der oft unterschätzt wird: UserProSubscription braucht eine zweite Provider-Quelle neben Stripe, inklusive Receipt-/Webhook-Validierung, Plan-Reconciliation und Kollisionsregeln für „hat im Web und in der App gekauft". Dazu Apple 3.1.2(a): das Abo muss auf allen Geräten des Nutzers funktionieren. Der Store wird Merchant of Record — andere Steuerlage, parallele Abo-Verwaltung, zwei Kündigungswege.
  • Bei geringer mobiler Conversion rechnet sich das nicht: die Differenz von ~0,40 € × erwartete mobile Neuabschlüsse gegen mehrere Wochen Entwicklung plus dauerhafte Wartung an zwei Abrechnungspfaden.

Erlaubt externe Kauflinks in EU-Storefronts, kostet aber real ~10–12 % (Initial Acquisition Fee + Store Services Fee + Core Technology Commission), verlangt das systemseitige Disclosure-Sheet, ein Produktseiten-Banner und die Meldung jeder Transaktion über die External Purchase Server API binnen 15 Kalendertagen — und schließt IAP in derselben Storefront aus. Bei Google gibt es Gegenstücke (Billing-choice-Programm bzw. External-offers-Programm, ~10 % auf Abos, 24-Stunden-Reporting, teils Business-Entity-Registrierung Pflicht). Unsicher: Der Übergang von Core Technology Fee zu Core Technology Commission war zum 01.01.2026 angekündigt; ob er Mitte 2026 vollzogen ist, ist widersprüchlich berichtet. Ebenso überlagern sich bei Google gerade Billing-choice- und External-offers-Programm. Vor jeder Entscheidung im Developer-Account bzw. in der Play Console gegenprüfen.

➜ EMPFEHLUNG: Option A für v1.0

Bei 5,99 € Abopreis und Solo-Betrieb ist der Provisionsvorteil von B/C gegenüber A negativ — A kostet 0 % und 0 Zeilen Code, B kostet 15 % und mehrere Wochen Arbeit und dauerhafte Doppelwartung. Pro-Käufe entstehen bei TargetShot ohnehin über Verein und Web-Onboarding, nicht als mobiler Impulskauf.

Konkret zu tun (Store-Hygiene, sonst Ablehnung nach 3.1.1): 1. Den Button „Upgrade folgt in Mobile" entfernen (src/screens/ProScreen.tsx:58). Ein Upgrade-Button ist eine Call-to-action, auch wenn er funktionslos ist — Apple prüft auch auf ruhende Kauffunktionalität. 2. Den Satz „…können sie aber erst nach einem Upgrade voll verwenden" (Zeile 41) umformulieren, Wort „Upgrade" streichen. 3. „Gesperrt" (Zeile 66) neutral fassen, z. B. „In deinem Tarif nicht enthalten". 4. Besser noch: Free-Nutzern den Pro-Tab gar nicht erst zeigen, statt ihn gesperrt darzustellen. 5. Keine Preise, keine Links auf /pricing oder Checkout in der App.

Trotzdem einen Einspruch vorbereiten. Es ist dokumentiert, dass Login-only-Companion-Apps ohne jeden Kauf-Flow gelegentlich unter 3.1.1 abgelehnt werden („Ihre App greift auf außerhalb erworbene digitale Inhalte zu…"). Der Einspruch verweist auf 3.1.3(f) und beschreibt den Sachverhalt in zwei Sätzen. Ein bis zwei Ablehnungsschleifen sind einzuplanen, keine Panik.

Option B bleibt als eigener Meilenstein nach dem ersten Store-Release offen — dann mit RevenueCat oder expo-iap und einer sauberen zweiten Provider-Quelle im Backend.


3. Weitere Entscheidungen

3.1 Apple-Konto: Individual oder Organization

Option Folgen
Individual (99 USD/Jahr) Kein D-U-N-S nötig. Aber: der bürgerliche Name erscheint als Verkäufer im App Store.
Organization Verlangt eingetragene Rechtsform plus D-U-N-S. Firmenname als Verkäufer. Nachträglicher Wechsel ist aufwendig.

Zusätzlich unabhängig davon: EU-DSA-Trader-Status ist seit 17.02.2025 Pflicht; Apple hat Apps ohne verifizierten Status aus dem EU-Store entfernt. Verifiziert werden Adresse, Telefonnummer und E-Mail — und diese werden öffentlich auf der App-Store-Produktseite in allen 27 EU-Ländern angezeigt.

Empfehlung: Individual, aber vorher eine ladungsfähige Geschäftsadresse besorgen (Postfach oder Impressumsservice), sonst steht die Privatadresse öffentlich im Store. ⚠️ Widerspruch in den Quellen: Ob Apple für Individuals ein Postfach als Adresse akzeptiert, wird gegensätzlich berichtet. Vor dem Enrollment beim Apple-Support klären (Abschnitt 8).

3.2 Google-Play-Konto: Personal oder Organization

Option Folgen
Personal (25 USD einmalig) 12 Tester müssen 14 zusammenhängende Tage durchgehend opt-in bleiben, bevor Produktionszugang beantragt werden kann (gilt für Konten, die nach dem 13.11.2023 erstellt wurden).
Organization Von der Testerpflicht ausgenommen — verlangt aber gültige D-U-N-S und Nachweis einer registrierten Rechtsperson.

Empfehlung: Personal-Konto nehmen und die 14 Tage früh und parallel starten (siehe Etappenplan). Ob ein deutscher Einzelunternehmer ohne Handelsregistereintrag die Organization-Voraussetzungen erfüllt, ist unklar — im Zweifel nicht darauf warten. Tester aus den Beta-Vereinen rekrutieren (z. B. Club 413067), mehr als 12 einladen, weil Abmelder mitzählen.

3.3 Push: Expo Push Service oder APNs/FCM direkt

Option Für Gegen
Expo Push Service (expo-server-sdk) Schnell, ein SDK, Batching zu 100, Tickets/Receipts, DeviceNotRegistered-Aufräumen Ein weiterer Anbieter in der Kette, Payloads laufen über Expo-Server
APNs (.p8/JWT) + FCM v1 direkt Konsistent mit der Selbst-Hosting-Linie des Projekts, keine Drittpartei im Zustellpfad Mehr Arbeit, zwei Credential-Sätze, Legacy-FCM ist abgeschaltet

Empfehlung: Expo Push Service für v1 der Push-Funktion. Der Datenschutz-Unterschied ist beherrschbar (Payload bewusst inhaltsarm halten: Typ + ID, Details erst nach Öffnen aus der eigenen API), der Zeitgewinn ist erheblich. Migration auf APNs/FCM direkt bleibt später möglich, wenn das Datenmodell die Spalte provider enthält.

3.4 Umfang von v1.0

Option Bewertung
Nur der heutige Stand + Pflichtfixes Schnellster Store-Launch. Aber ohne Push und ohne Challenges ist die App „nur ein kleineres Web".
Heutiger Stand + Consent + Push + Bestenliste + Challenges/Duelle Der eigentliche mobile Produktnutzen. Deutlich später.

Empfehlung: gestaffelt. v1.0 = heutiger Stand + Consent-Gate + Auth-Fixes + Store-Hygiene → früh in beide Stores, damit die Konten-, Review- und Play-Test-Uhren laufen. v1.1 = Push + Bestenliste + Challenges/Duelle. Begründung: Challenges/Duelle sind per Definition ein Push-Feature (Wochenrhythmus, Duellstände, Saison-Pass) — sie ohne Push auszuliefern verschenkt genau den Hebel, für den man sie baut. Bestenliste ist read-only und passt praktisch 1:1 auf einen Handy-Screen, also die billigste v1.1-Fläche.

3.5 Der Verein-Tab

Empfehlung: für v1.0 aus der Tab-Leiste entfernen (4 Tabs statt 5). Mitgliederverwaltung, Vereinseinstellungen, Vereinslizenz/Abrechnung, Vereinsreports und Standaufsicht bleiben Desktop — sie sind breite Tabellen-, Formular- und Vorstandsarbeit. Ein Platzhalter-Tab mit „Hier folgen später…" ist im Store schlechter als kein Tab. Später ggf. als read-only Trainer-Sicht wieder aufnehmen (Kohorten-Kennzahlen, k-anonymisiert).

3.6 i18n

Empfehlung: v1.0 mit deutschem Store-Listing ausliefern, Strings weiter hart deutsch. Sobald ein englisches Listing kommt, i18n einführen — und im gleichen Zug den CI-Wächter mitziehen (siehe Fallstricke 7.2). Ein halbherziges i18n ohne Wächter ist schlechter als keins.

3.7 SDK-Sprung 54 → aktuell

Empfehlung: vor der Feature-Arbeit, als eigener abgesicherter Schritt. Drei übersprungene Releases in einem Rutsch, mit RN-Sprung und Breaking Changes in expo-router (z. B. resetresetOnFocus) sowie einer neuen Xcode-Mindestversion. Danach neue Dev-Builds erzeugen. Unsicher: Welche SDK-Version aktuell die stabile ist (57 wird berichtet), gilt es zu Beginn der Session auf expo.dev/changelog zu prüfen — nicht aus dem Gedächtnis annehmen.

Heute: targetshot-mobile://auth/callback. Nach RFC 8252 zulässig, und PKCE entwertet einen abgefangenen Code — aber eine Fremd-App kann dasselbe Scheme registrieren. Empfehlung: v1.0 mit Custom Scheme starten. Universal Links / App Links (https://web.targetshot.app/... öffnet die App) für v1.1 — erst dann landen Mail-Links und geteilte Duell-Links in der App statt im Browser. Braucht apple-app-site-association und assetlinks.json auf der Domain.

3.9 Web-Push: reparieren oder ehrlich entfernen

Der Web-Push-Pfad ist ein Zombie: Frontend registriert Service Worker und schickt Subscriptions, Backend kann nichts damit anfangen, weil web-push nicht installiert ist. Empfehlung: entfernen, wenn nicht binnen eines Tages repariert. Ein Endpunkt, der Daten annimmt und nie zustellt, ist eine Falle für die nächste Person — und UserPushSubscription speichert nutzlos personenbeziehbare Daten.

3.10 Kontolöschung in der App

Apple 5.1.1(v) verlangt einen In-App-Löschpfad, wenn die App Kontoerstellung unterstützt. TargetShot erstellt Konten nicht in der App — Reviewer wenden die Regel aber häufig auch bei extern angelegten Konten an. Google verlangt zusätzlich eine öffentlich erreichbare Web-URL für Löschanträge. Empfehlung: beides bauen. Der Web-Löschpfad existiert bereits inklusive Stripe-Kündigung — im Profil-Tab sichtbar verlinken bzw. anstoßen, plus eine öffentliche Lösch-URL für das Play-Formular.


4. Technischer Weg

Empfehlung: Expo weiterführen. Nicht Capacitor, nicht Bare React Native.

Warum nicht Capacitor — vier konkrete Repo-Gründe, nicht generische: 1. apps/web ist eine Desktop-orientierte SPA (73.296 LOC, 92 Seiten-Komponenten, react-router-dom 7, Sidebar-Layouts) und war nie als App-Shell entworfen. 2. Der Web-Login hängt an keycloak-js mit check-sso-Iframe und public/silent-check-sso.html. Im WKWebView unter capacitor://-Origin funktioniert das nicht ohne Umbau auf browserbasiertes PKCE — und Login im In-App-WebView verstößt gegen RFC 8252 und ist im Apple-Review riskant. 3. Apple 4.2 (Minimum Functionality) trifft reine Website-Wrapper hart. 4. Man hätte zwei parallele Mobile-Stränge im selben Repo, bei einem Solo-Entwickler.

Warum nicht Bare RN: Es wird kein exotisches Native-Modul gebraucht — Auth, Push und (falls später) IAP sind alle als Expo-Modul oder Config-Plugin verfügbar. Die manuelle Xcode-/Gradle-Toolchain wäre der teuerste Teil, und genau den nimmt EAS Build ab.

Warum Expo: 10.492 Zeilen laufender Code mit funktionierendem PKCE-Login, Tab-Navigation, Theme, Dashboard-Auswertung und Inbox — plus eas.json und CI-Absicherung. Das wegzuwerfen wäre die teuerste denkbare Entscheidung.

Was wirklich wiederverwendbar ist

Heute geteilt: nur @targetshot/core (73 Zeilen) — normalizePlanValue, hasProAccess, hasClubAccess, getPlanLabel, plus ein paar ungenutzte Spielzeug-Typen (scoreSession, formatTimestamp), die mit dem echten Domänenmodell (DashboardSession, Teiler, Deci-Ringe) nichts zu tun haben. Mobile importiert es an 3 Stellen, Web an genau einer (apps/web/src/utils/plan.ts:6).

Geteilt als Vertrag, nicht als Code: die REST-API (Bearer-JWT, kein SSR, keine Cookies), der Notification-Inbox-Vertrag (docs/notification-contract.md), /api/user/settings, /api/dashboard/shooting, und der Keycloak-Realm.

Sieht geteilt aus, ist es aber nicht — die eigentliche Gefahr:

  • Dashboard-Typen sind zwei Kopien ohne Wächter. DashboardSummary/Series/Hit/Session/StarterlistCategory stehen wortgleich in apps/web/src/pages/DashboardTypes.ts und mobile/src/api/dashboard.ts:3-74 — mit bereits sichtbarer Drift (Feldreihenfolge lastName/firstName vertauscht; Web zieht zusätzlich PersonalClubOption). Ändert das Backend ein Feld, bricht Mobile still.
  • statisticsModel.ts ist heute NICHT wiederverwendbar, obwohl es inhaltlich reine Rechnung ist (1.133 Zeilen). Die Kette: statisticsModel.ts../utils/targetsWertimport (kein import type!) von ISSF_AIR_RIFLE, ISSF_AIR_PISTOL aus ../components/Charts — einer 624-Zeilen-React/SVG-Komponentendatei. Metro würde beim Import React-DOM-Komponenten ins RN-Bundle ziehen. Dasselbe gilt für statisticsInsightsModel.ts, statisticsProModel.ts, clubOverviewModel.ts (enthält wholeRingScoreForHit, den Deci-Ring-Helper) und dashboardSessionModel.ts. Zusätzlich zieht statisticsModel.ts resolveStarterlistCategory aus einer .tsx-Datei.
  • i18n: die Kataloge de.ts/en.ts (je ~2.900 Zeilen) sind portable Daten, der Provider i18n/index.tsx ist es nicht (localStorage, startTransition-Dict-Swap, dynamischer Import).
  • @targetshot/ui ist bewusst web-first und ausgeschlossen. CSS, Motion-Lib, DOM-Charts sind Neubau in RN — react-native-svg liegt bereits bereit.
  • Auth-Semantik doppelt: Rollen aus realm_access/resource_access und Plan aus targetshot.plan werden in apps/web/src/auth/authIdentity.ts (306 Z.) und in mobile/src/auth/context.tsx (parseJwtClaims/extractRoles) unabhängig abgeleitet. Der Web-Zweig hat dafür 1.303 Zeilen Tests, der Mobile-Zweig keine.

Extraktionsplan nach packages/core (in dieser Reihenfolge)

  1. Claim-Parsing + Rollen-/Plan-Ableitung — billig, verhindert sofort Drift, hat auf Web-Seite Tests als Netz.
  2. Dashboard-DTO-Typen — reine type-Extraktion, null Laufzeitrisiko, beseitigt die akuteste stille Bruchstelle.
  3. Zielgeometrie-Konstanten (ISSF_AIR_RIFLE, ISSF_AIR_PISTOL, TargetSpec) aus components/Charts.tsx herauslösen, dann detectDiscipline/targetSpecFor und resolveStarterlistCategory nachziehen.
  4. wholeRingScoreForHit / Deci-Ring-Logik — löst gleichzeitig eine bekannte Fehlerklasse (siehe 7.6).
  5. Erst dann statisticsModel & Co.

Das Werkzeug dafür existiert bereits: frontend/tools/check-package-boundaries.mjs erzwingt apps → packages, packages/core hängt an nichts, Alias-Pflicht statt relativer ../../packages-Pfade. SCAN_ROOTS muss um 'mobile' erweitert werden — dann fällt auch der Cross-Import in ProfileChangelogScreen.tsx:2 sofort auf.

Mobile-API-Client härten

mobile/src/api/client.ts ist 29 Zeilen und hat drei Sperren: - Zeile 13 setzt Content-Type: application/json hart → multipart bricht (blockiert Avatar-/Logo-Upload) - Zeile 28 ruft immer response.json()Binärantworten unmöglich (blockiert POST /api/export/pdf) - Kein 401-Retry — anders als apps/web/src/utils/api.ts (authorizedFetch mit forceRefresh-Retry, recoverSession, handleUnauthorized). Nach längerem Hintergrund kann der erste Request hart fehlschlagen.


5. Was VOR dem ersten Code erledigt sein muss

Konten & Identität

  • [ ] Ladungsfähige Geschäftsadresse / Postfach entscheiden und besorgen (DSA-Trader macht sie öffentlich)
  • [ ] Beim Apple-Support klären, ob ein Postfach für Individual-Enrollment akzeptiert wird (Quellen widersprechen sich)
  • [ ] Apple Developer Program eröffnen, Individual, 99 USD/Jahr, Identitätsprüfung durchlaufen
  • [ ] DSA-Trader-Status in App Store Connect eintragen und verifizieren lassen (ohne ihn kein EU-Vertrieb)
  • [ ] Google Play Console eröffnen, 25 USD einmalig, Identitätsprüfung (Ausweis, ggf. Selfie, 2FA), öffentlichen Entwicklernamen festlegen
  • [ ] Google DSA-Trader-/Händlerangaben mit verifizierter Adresse
  • [ ] Expo-Account + Organisation anlegen (owner in app.config.ts)

Keycloak (manueller Schritt außerhalb des Repos — bitte dokumentieren!)

  • [ ] Prüfen, ob der Client targetshot-mobile in Prod-Keycloak überhaupt existiert — aus dem Repo nicht belegbar
  • [ ] Client-ID targetshot-mobile, Client authentication = OFF (public client)
  • [ ] Standard Flow = ON, Direct Access Grants = OFF
  • [ ] Advanced → PKCE Code Challenge Method = S256
  • [ ] Valid Redirect URIs = exakt targetshot-mobile://auth/callback, keine Wildcards
  • [ ] Client Scope offline_access zulassen — sonst kommt trotz angefragtem Scope kein Refresh-Token
  • [ ] „Use refresh tokens" aktiv
  • [ ] Web Origins nicht nötig (kein Browser-CORS, Token-Call läuft nativ)
  • [ ] Für Dev-Builds ggf. zusätzlichen Expo-Dev-Redirect registrieren
  • [ ] Ergebnis als Skript oder mindestens als ADR/Vault-Notiz festhalten — sonst ist es beim nächsten Realm-Neuaufbau weg

Zertifikate & Credentials

  • [ ] eas initextra.eas.projectId in app.config.ts eintragen
  • [ ] iOS Distribution Certificate + Provisioning Profile (kann EAS verwalten)
  • [ ] Android Upload-Keystore (kann EAS verwalten) — Backup außerhalb von EAS ablegen
  • [ ] (nur bei Push) APNs Auth Key (.p8), Key-ID, Team-ID
  • [ ] (nur bei Push) Firebase-Projekt mit identischem Package-Namen app.targetshot.mobile + FCM-v1-Service-Account-JSON
  • [ ] EXPO_PUBLIC_TARGETSHOT_*-Werte für Beta und Prod getrennt festlegen (fail-closed, kein Fallback!)

Store-Einträge & Assets

  • [ ] App-Icon 1024×1024, Android Adaptive Icon (Vorder-/Hintergrund), Splash — existiert heute nichts, kein assets/-Verzeichnis
  • [ ] Screenshots für alle geforderten Gerätegrößen — ohne Waffenfotos (Apple 2.3.8: Icon/Screenshots/Previews müssen 4+-tauglich sein, auch bei höher eingestufter App)
  • [ ] Store-Beschreibung DE, Keywords, Support-URL, Marketing-URL
  • [ ] Altersfreigabe: Apple-Fragebogen (neue Skala 4+/9+/13+/16+/18+), Google IARC-Fragebogen → USK. Realistisch 4+/9+ bzw. USK 0/6 — Trefferbilder sind keine „realistic violence"
  • [ ] Google-Zielgruppen-Fragebogen: bewusst entscheiden, ob Kinder unter 13 adressiert werden (löst Families-Policy mit Zusatzauflagen aus — relevant wegen der Jugend-Funktionen)
  • [ ] Datenschutzerklärung- und AGB-URL auf Prod-Domain (heute beta.targetshot.app in AuthLandingScreen.tsx:99,103)
  • [ ] Apple „App Privacy"-Deklaration ausfüllen (Keycloak-Identität, Trainingsdaten, Benachrichtigungen, Einstellungen)
  • [ ] Google „Data Safety"-Formular ausfüllen — muss konsistent zur App-Privacy-Angabe sein
  • [ ] Öffentliche Konto-Lösch-URL bereitstellen
  • [ ] Review-Notes vorbereiten: „kostenlose Companion-App zu einem bezahlten Web-SaaS, kein Kauf und keine Kaufaufforderung in der App" mit Verweis auf 3.1.3(f)
  • [ ] 12+ Tester für das Play Closed Testing rekrutieren und Einladungsliste bereithalten

6. Etappenplan

Jede Etappe hat ein überprüfbares Ergebnis — nicht „fertig", sondern ein Testsatz, der grün ist oder nicht.

E0 — Konten & Zugänge (läuft parallel zu allem, blockiert nichts im Code)

Alle Punkte aus Abschnitt 5 „Konten & Identität" und „Keycloak". Ergebnis: Apple-Enrollment aktiv, Play-Konto verifiziert, DSA-Trader-Status bei beiden gesetzt — und ein manueller Login vom echten Gerät gegen Prod-Keycloak mit dem Client targetshot-mobile hat funktioniert.

E1 — Entfrosten und store-fähig machen

Sauberer npm ci in frontend/, Typecheck-Fehler in app/(tabs)/_layout.tsx:26 verifizieren (erwartet: verschwindet mit frischem Install). SDK-Sprung. expo-dev-client nachinstallieren. eas init → projectId. Icon, Splash, Adaptive Icon anlegen. updates/runtimeVersion festlegen. Ergebnis: eas build --profile preview läuft für beide Plattformen durch; die App ist auf einem echten iPhone und einem echten Android-Gerät installiert und startet; Login gegen Prod-Keycloak funktioniert.

E2 — Auth-Mängel und API-Client

End-session-Endpoint im signOut (mit id_token_hint), expectedNativeRedirectUri tatsächlich als Guard verwenden (mit verständlicher Fehlermeldung statt stiller Fehlschlag in Expo Go), 401-Retry mit forceRefresh im apiRequest, multipart- und Binär-Pfad im Client. Ergebnis, als Handtest: (a) Logout → erneuter Login zeigt wieder die Keycloak-Anmeldemaske, nicht stillen Auto-Login. (b) App 24 h im Hintergrund → erster Request lädt Daten statt zu scheitern. (c) docs/mobile-release-checklist.md und README.md beschreiben, was der Code wirklich tut.

Vollbild-Consent-Gate direkt nach dem Login analog zum Web-Gate, Widerrufsmöglichkeit, Behandlung von 403 consent_required als eigener Zustand, Jugend-/Erziehungsberechtigten-Fall. Datenschutz-/AGB-Links auf Prod-Domain. Konto-Löschpfad im Profil. Ergebnis: Ein Testkonto mit widerrufener Einwilligung sieht die Einwilligungsstrecke — nicht leere Screens. Ein Widerruf mobil wirkt sich im Web aus und umgekehrt.

E4 — Store-Hygiene

Pro-Tab für Free-Nutzer ausblenden bzw. Upgrade-CTA restlos entfernen. Ergebnis: grep -rniE 'upgrade|5,99|59 €|jetzt kaufen|abo abschließen|pricing' frontend/mobile/src frontend/mobile/app liefert keinen Treffer.

E5 — Play Closed Testing starten (so früh wie möglich!)

Sobald aus E1 ein installierbarer Android-Build existiert: Closed Track anlegen, >12 Tester einladen, Release freigeben. Ergebnis: In der Play Console laufen 12 opt-in-Tester, der 14-Tage-Zähler ist gestartet. Diese 14 Tage laufen parallel zu E2–E6 ab, nicht danach — das ist der längste unvermeidbare Pfad der ganzen Veröffentlichung.

E6 — Extraktion nach packages/core

Schritte 1–2 des Extraktionsplans (Claim-Parsing/Plan, Dashboard-Typen), SCAN_ROOTS um 'mobile' erweitern, Cross-Import in ProfileChangelogScreen.tsx auflösen. Ergebnis: npm run check:boundaries erfasst frontend/mobile und ist grün; die Dashboard-DTOs sind nur noch einmal im Repo deklariert; Web-Tests bleiben grün.

E7 — Erst-Einreichung v1.0

TestFlight-Phase parallel zum Play-Test, dann beidseitig einreichen. Ergebnis: App in beiden Stores öffentlich verfügbar. Erwartungsmanagement: Apple nennt >90 % Reviews unter 24 h, Erst-Einreichungen dauern realistisch 24–72 h, teils 5–7 Tage; Play-Erst-Einreichungen neuer Konten 3–7 Tage. Ein bis zwei 3.1.1-Ablehnungsschleifen einplanen — Einspruch mit 3.1.3(f)-Begründung liegt vorbereitet.

E8 — Push (v1.1, Vorarbeit kann früher laufen)

Neues Prisma-Modell (Vorschlag: UserDevicePushToken { userId, token, platform: ios|android, provider, appVersion, locale, lastSeenAt, disabledAt }), Registrierung mit getExpoPushTokenAsync({ projectId }), Fan-out-Schicht, die createOrUpdateUserNotification (Inbox) und Device-Push koppelt, Tickets/Receipts auswerten und DeviceNotRegistered abräumen, Deep-Link-Auflösung auf das vorhandene actionUrl-Feld, Badge-Count, Opt-in-Kategorien in /api/user/settings. Toten Web-Push-Pfad reparieren oder entfernen. Ergebnis: Eine echte Push kommt auf iPhone und Android an, öffnet per Deep Link den richtigen Screen, respektiert das Opt-in, und ein deinstalliertes Gerät wird nach dem nächsten Versand automatisch deaktiviert. Realistischer Umfang: 3–5 Tage.

E9 — Feature-Delta (v1.1)

Bestenliste (/api/leaderboard) zuerst — read-only, passt 1:1 auf einen Screen. Dann Challenges/Duelle (/api/challenges, /season/pass, /season/leaderboard, /collection, /duels) inkl. aller vier Duell-Modi. Ergebnis: Ein Duell lässt sich mobil vollständig verfolgen; der k-Anon-Unterdrückungszustand hat eine eigene, verständliche Darstellung (siehe 7.5).

E10 — CI/Release-Pfad

EAS-Build-Job im Workflow, Versionskopplung zum Web-Release, RN-Testing-Library plus mindestens Smoke-Renderer für die Kern-Screens, vitest.config.ts include um .tsx erweitern. Ergebnis: Ein Tag/Release erzeugt automatisch einen Store-Build, und mindestens ein UI-Test rendert tatsächlich eine Komponente.

Grobe Zeitachse: 6–10 Wochen bis zur öffentlichen Verfügbarkeit in beiden Stores, wenn E0/E5 früh und parallel laufen. Die zwei Terminfresser sind der 14-Tage-Play-Test und Apples 3.1.1-Risiko — beide sind nicht durch mehr Arbeitszeit abkürzbar.


7. Bekannte Fallstricke aus diesem Projekt, die mobil wiederkommen

7.1 ESM-Endungen im Backend

Prod läuft als node dist/… und braucht .js an relativen Imports. Weder tsc noch tsx fangen das ab — nur ein echter Prod-Build plus Node-Resolution. Betrifft mobil indirekt, aber sicher: jede neue Backend-Route für Device-Token, Push-Fan-out oder IAP-Reconciliation. Vor jedem Backend-Deploy: grep auf relative Imports ohne Endung, npm run build, Node-Import testen. Mobiles Analogon: Alles, was neu nach packages/core wandert, muss für Metro und für tsc auflösbar sein. Metro hat im Monorepo eine eigene Konfiguration (metro.config.js, inkl. React-Aliasing aufs Workspace-Root) und ein eigenes Babel-Plugin für expo-router. Ein Import, der im Web-Build funktioniert, kann im RN-Bundle scheitern — und umgekehrt.

7.2 i18n-Schlüssel, die als Schlüssel rendern

Es gibt einen CI-Schritt, dessen Name das Problem exakt beschreibt: „Verify i18n keys resolve (missing key would render as raw key)" (.github/workflows/ci.yml, apps/web/scripts/verifyI18nKeys.ts). Ein fehlender Key erzeugt keinen Fehler, sondern zeigt dem Nutzer den rohen Schlüssel. Mobile hat heute kein i18n und daher keinen Wächter. Wenn i18n mobil eingeführt wird, muss der Wächter im selben PR mitkommen — sonst steht irgendwann pro.features.reports.title im Store-Screenshot.

7.3 Stripe-API-Eigenheiten

Zwei bekannte, teuer bezahlte Eigenheiten: (a) das Account-Objekt hat kein livemode — Live/Test ist daran nicht erkennbar. (b) Die Portal-Konfiguration liefert products nicht zurück; ob die richtige Konfiguration greift, ist nur über eine echte Portal-Sitzung prüfbar. Es gibt bereits getrennte Portal-Konfigurationen je Flow (Verein vs. Pro). Mobil relevant, falls doch IAP kommt: dann existiert UserProSubscription mit zwei Quellen. Es braucht explizite Kollisionsregeln („Web-Abo aktiv, Nutzer kauft in der App erneut") und eine Antwort darauf, welcher Kanal beim Kündigen gilt. Nicht implizit lösen.

Der Consent-Gate ist im Web ein harter Vollbild-Blocker vor jeder Route (apps/web/src/App.tsx:98-101), und das Backend blockt denselben Zustand serverseitig mit 403 (backend/src/services/shootingDataAccess.ts:173). Es gibt außerdem consent-abhängige Verdeckung in bezahlten Flächen: Duelle brauchen beidseitiges GRANTED, sonst 403 und verdeckte Werte (valueHidden/consentBlocked); bei BEST_OF_3 verdeckt ein einziger Widerruf beide Seiten. Berichte haben ein Consent-Gate im geplanten Versand plus Auto-Deaktivierung. Mobil heißt das: Consent ist kein Feature, sondern ein Zustand, den jede Datenfläche kennen muss. „Keine Daten" und „nicht eingewilligt" sind zwei verschiedene UI-Zustände. Der Nutzer muss auf beiden Wegen zur Einwilligungsstrecke kommen.

7.5 k-Anonymität — mobil ein eigener UI-Zustand, kein Leerzustand

Verifiziert im Code: - backend/src/routes/dashboard.tsMIN_BENCHMARK_COHORT_OTHERS = 3. Vereins-Aggregate werden erst ab 3 ANDEREN eingewilligten Schützen ausgeliefert; der eingeloggte Nutzer zählt nie in die Kohorte hinein. Sonst kommt { available: false, reason: 'insufficient_cohort', requiredOthers } — bewusst ohne de-anonymisierende Zahl. - backend/src/services/seasonLeaderboard.ts: nur Mitglieder mit consentVisible erscheinen; unterhalb der Schwelle wird die volle Liste unterdrückt, aber die eigene Position bleibt immer sichtbar, und self zählt nie in die Schwelle.

Der Fehler, den man mobil garantiert macht: available: false als „keine Daten" oder als Ladefehler rendern. Richtig ist eine eigene, erklärende Darstellung („Vereinsvergleich zeigen wir erst ab 3 weiteren eingewilligten Schützen"). Und: niemals eine Ersatzzahl anzeigen, um den Screen zu füllen — genau das würde die Anonymität aushebeln, die der Backend-Code schützt.

7.6 Deci-Ring und Probe- vs. Wertungsschüsse — doppelte Rechenlogik

Die Session-/Serien-/Teiler-Auswertung existiert zweimal: mobile/src/utils/sessions.ts (345 Zeilen) gegen die Web-Seite. Da hängen fachliche Regeln drin (Probe- vs. Wertungsschüsse, Deci-Ring, Ganzring-Summe), und die Deci-Ring-Behandlung ist auf Web-Seite bereits einmal als Fehlerklasse aufgefallen — behoben durch einen Backend-Helper sumWholeRingFromHits. Jede künftige Korrektur muss heute zweimal gemacht werden. Das ist der eigentliche Grund für Extraktionsschritt 4 in Abschnitt 4.

7.7 GH-Actions maskiert Job-Outputs mit Secret-Anteil

Jeder Job-Output, der einen secrets.*-Wert enthält, kommt im nächsten Job leer an. Relevant, sobald der EAS-Build-Job kommt (EXPO_TOKEN, Keystore-Passwörter, App-Store-Connect-Key): niemals zusammengesetzte Werte mit Secret-Anteil cross-job weiterreichen.

7.8 Branch vor neuer Arbeit

Nach einem Prod-Release steht HEAD auf main. Erst einen Feature-Branch auschecken, sonst committen die ersten Mobile-Edits direkt auf main.

7.9 Fail-closed Runtime-Config ist gut, aber ungeprüft

app.config.ts liest fünf EXPO_PUBLIC_TARGETSHOT_*-Variablen ohne Fallback; fehlt eine, blockt hasMobileRuntimeConfig. Für zwei Zielumgebungen (Beta/Prod) braucht es zwei gepflegte EAS-Profile plus eine automatische Prüfung im Release-Prozess. Heute ist das eine README-Checkliste, kein Schritt — und ein Build mit leerer Variable fällt erst auf dem Gerät auf.

7.10 Chromium kann iOS nicht verifizieren

Aus der Web-Arbeit bekannt: das Browser-Pane spiegelt iOS-Safari-Verhalten (z. B. Formularelement-Breiten) nicht. Für Mobile gilt das erst recht — „visuell verifiziert" darf nur behauptet werden, was auf echtem Gerät oder im iOS-Simulator gesehen wurde. Der Simulator ist über das vorhandene iOS-Simulator-Tooling erreichbar.


8. Offene Fragen mit Zuständigkeit

# Frage Warum offen Zuständig Blockiert
1 Existiert der Keycloak-Client targetshot-mobile in Prod? Im Repo nur als erwarteter Wert dokumentiert, keinerlei Provisionierungscode Gründer (Keycloak-Admin) E1 — ohne ihn kein Login vom Gerät
2 Akzeptiert Apple ein Postfach als Adresse beim Individual-Enrollment? Quellen widersprechen sich direkt Gründer, via Apple-Support vor dem Enrollment E0, und damit alles
3 Ist der App-Store-Verkäufername = Klarname akzeptabel? Individual zeigt den bürgerlichen Namen; Wechsel auf Organization ist nachträglich teuer Gründer E0
4 Kann ein deutscher Einzelunternehmer ohne HR-Eintrag ein Play-Organization-Konto bekommen (D-U-N-S)? Unklar; entscheidet über die 14-Tage-Testerpflicht Gründer, via Google/D&B E5 — im Zweifel nicht darauf warten
5 Adressieren die Jugend-Funktionen Nutzer unter 13? Löst im Play-Zielgruppen-Fragebogen die Families-Policy mit erheblichen Zusatzauflagen aus Gründer (Produktentscheidung) Store-Einträge
6 Web-Push reparieren oder entfernen? web-push ist nicht installiert, Endpunkt nimmt trotzdem Daten an Gründer + Agent, backend/src/notifier.ts, UserPushSubscription E8
7 Welche Expo-SDK-Version ist aktuell stabil? Wissensstand unsicher (57 wird berichtet); vor dem Sprung auf expo.dev/changelog prüfen Agent, zu Beginn von E1 E1
8 Stand des Apple CTF→CTC-Übergangs und der Überlagerung von Play Billing-choice / External-offers Widersprüchlich berichtet; nur bei IAP-Entscheidung relevant Gründer, im Developer-Account bzw. Play Console gegenprüfen nur Option B/C
9 Tatsächliche Stripe-Gebühren pro Abo Für die Margenrechnung in Abschnitt 2 geschätzt, nicht belegt Gründer, Stripe-Dashboard nur Option B
10 Sind die Challenge-/Duell-Endpunkte ohne web-spezifische Annahmen mobil nutzbar? Nicht geprüft; die Web-Flächen sind jung (PRs #49–#57) Agent, beim Bau von E9 E9
11 Wie viele Tester lassen sich realistisch aus den Beta-Vereinen gewinnen? Harte Untergrenze 12, Abmelder zählen mit Gründer E5
12 Braucht v1.0 eine Beta-Umgebung im Store-Build oder reicht Prod? Zwei EAS-Profile mit getrennten EXPO_PUBLIC_*-Werten sind vorgesehen, aber nie durchgespielt Gründer + Agent E1

Ein-Absatz-Zusammenfassung für den Agenten, der hier startet

Nimm nichts als Greenfield an. Lies zuerst frontend/mobile/README.md, docs/mobile-release-checklist.md und frontend/mobile/src/auth/context.tsx — und misstraue der Doku, sie behauptet an mindestens einer Stelle (Logout via end-session) etwas, das der Code nicht tut. Beginne mit einem sauberen npm ci in frontend/, nicht mit Code. Die Reihenfolge lautet: Konten und Keycloak klären (E0) → entfrosten und einen echten Build aufs Gerät bringen (E1) → Play-Closed-Test sofort starten (E5, läuft 14 Tage im Hintergrund) → Auth und Consent reparieren (E2, E3) → Store-Hygiene (E4) → einreichen. Push und Challenges kommen danach. Die Geschäftsentscheidung ist getroffen und lautet: v1.0 verkauft nichts — der Upgrade-Button in ProScreen.tsx muss weg, nicht verdrahtet werden.