UX-Welle nach User-Feedback aus dem ersten Live-Test der Mail-Page:
Page-Hierarchie neu (top → bottom):
1. HALF-DONUT als HERO-Karte — bisherige "BLOCKIERT XX über N Postfächer Live"-
Banner-Card weg, Inhalt ist jetzt Title-Zeile innerhalb der Donut-Karte
(rendert nur ab ≥2 Connections; Fallback-Stats-Row für 0-1 Connections)
2. Postfach-Liste (Account-Cards aus letztem Refactor — schlanker Header)
3. NEU: "Mehr Infos"-Collapsible — Bar-Chart "Blockiert letzte 30 Tage"
liegt jetzt versteckt drin (default collapsed)
4. Activity-Log "Kürzlich blockiert" (unverändert)
5. NEU: FAB unten rechts — 56pt brandOrange Kreis mit "+"-Icon,
öffnet ConnectMailSheet. Section-Header-Plus-Button entfällt.
Half-Donut Legend-Truncation:
- ≤3 Connections → alle anzeigen
- =4 Connections → alle anzeigen
- ≥5 Connections → Top-3 by blocked-count + "Sonstige"-Bucket
· Donut: 4 Segmente (Top-3 + OTHER_COLOR grau)
· Legend: 4 Zeilen (Top-3 fett, "weitere"-Zeile in regular grau)
Backend: GET /api/mail/stats/blocked-by-day?connectionId=<uuid> als
optionaler Filter (für per-Connection-Bar-Chart in expanded Account-Card,
in dieser Welle noch nicht im UI verdrahtet — Erweiterung kommt wenn
gewünscht).
FAB-Details (iOS-diskreter Shadow statt Material-Glow):
- position absolute, right 24, bottom = tabBarHeight + insets.bottom + 16
- 56pt, borderRadius 28, brandOrange BG, weißes Plus-Icon
- ScrollView paddingBottom angehoben damit kein Content unter dem FAB clipped
Edge-Cases:
- 0 Accounts → FAB sichtbar, Donut/Stats/Charts/Log versteckt + EmptyState
- 1 Account → Donut hidden (nur mit ≥2 Connections sinnvoll), Fallback-Stats-Row
- limitReached + FAB-Tap → bestehender Plan-Alert (FAB ist visuell nicht disabled)
Memory: Pull-to-refresh + bestehendes 30s-Status-Polling reichen für "wartet
auf erste verbindung"→"aktiv"-Übergang nach OAuth-Connect (Daemon-Heartbeat
braucht initial 2-9min, mo-Befund). UX-Polish-Option für später: in der
Initial-Phase einen freundlicheren "Verbinde gerade…"-Status anzeigen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Microsoft V2.0 OAuth-Spezifikation: ein einzelner /token-Exchange darf nur
Scopes EINES Resource-Servers enthalten. Unsere bisherige Scope-Liste
mischte:
https://outlook.office.com/IMAP.AccessAsUser.All (outlook.office.com)
User.Read (graph.microsoft.com)
Im /authorize akzeptiert MS das (Multi-Consent-Screen), aber beim Token-
Exchange wirft MS AADSTS70011:
"The provided value for the input parameter 'scope' is not valid.
One or more scopes [...] are not compatible with each other."
Fix: User.Read raus. Display-Name in der App entfällt vorerst — Email
kommt sauber aus id_token.preferred_username (bei Consumer-MS-Accounts
typisch die Login-Email). Falls Display-Name künftig gebraucht wird →
separater Graph-Token-Exchange via On-Behalf-Of-Pattern.
Plus: ConnectMailSheet zeigt jetzt im roten Error-Banner den echten
Backend-Error (API-Status + Body) statt nur generischen Text — sonst
würden wir solche MS-Spezifika nie auf dem Device sehen.
Hans-Müller-Memo Section 3.1 (Datenkategorien) + Section 4.1
(Datenschutzerklärung) müssen entsprechend zurückgerollt werden — siehe
separater DSB-Update-Stream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Microsoft hat App-Passwords für consumer-Outlook im September 2024 abgeschaltet.
Diese Welle bringt OAuth2/XOAUTH2-Support als zweiten AuthMethod-Pfad — Gmail/
iCloud/GMX/Yahoo bleiben unangetastet auf App-Password.
Backend (rebreak-backend):
- POST /api/mail/oauth/microsoft/init: PKCE-Flow-Start, generiert
code_verifier + Authorization-URL, persistiert pending state mit TTL
- POST /api/mail/oauth/microsoft/callback: Token-Exchange (PKCE, kein
client_secret weil Public Client), id_token-Decode für Email, MailConnection
upsert mit auth_method='oauth2_microsoft' + encrypted Tokens
- Token-Refresh-Util backend/server/utils/ms-oauth.ts + DB-Function
refreshAndSaveTokens(connectionId, clientId) mit optimistic-concurrency-
Race-Condition-Schutz (UPDATE WHERE oauth_token_expiry = <gelesener-wert>,
bei affected_rows=0 → frischen Wert lesen statt nochmal refreshen sonst
invalid_grant via Token-Rotation)
- Neue Tabelle oauth_pending_states (TTL via createdAt + Cleanup-Job-TODO)
- [id].delete.ts: echter OAuth-Disconnect — DB-Token-Löschung + Audit-Log
(MS hat keinen Drittanbieter-Revoke-Endpoint, daher User-Information-Pflicht
per Frontend-Modal, siehe DSB-Memo Section 5.1)
- Consent-Gate auch in scan.post.ts + scan-internal.post.ts (Cron-Trigger
war ohne Consent-Check = DSGVO-Lücke, jetzt geschlossen mit
skippedNoConsent-Field in Response)
IDLE-Daemon (backend/imap-idle/index.mjs, mo):
- XOAUTH2-Auth-Branch via getCredentialsForConnection() — wenn
auth_method='oauth2_microsoft', Token-Expiry-Check (<5min remaining →
proaktiver Refresh), sonst decrypted accessToken zu ImapFlow
- AUTHENTICATIONFAILED-Recovery: bis 3× reaktiv refresh + reconnect, danach
last_connect_error='auth_revoked' (kein Endlos-Loop)
- IDLE_RENEW_INTERVAL_MS = 10min — passt für MS 29min-Timeout (gleich wie
Gmail/iCloud)
- Consent-Pause: Connections mit consent_at=null laufen IDLE weiter (für
exists-Event-Wiederaufnahme), aber triggerScan() ist deaktiviert bis
consent erteilt
- start-idle-staging.sh: MS_OAUTH_CLIENT_ID explizit weiterleiten in den
inneren bash -c-Block (war Infisical-Var, ging aber durch strict-mode
verloren)
Frontend (rebreak-native-ui):
- Outlook-Tile re-aktiviert (war disabled mit "Kommt bald" seit Sept-2024-
Awareness), authMethod-Discriminator löst statt Email+Pw-Form den
OAuth-Flow aus
- ConnectMailSheet: neuer view-State 'oauth_warning' (Outing-Effekt-Hinweis
per Hans-Müller-Memo Section 6.1) + 'oauth_pending' (Browser-Step-Spinner)
- Deep-Link-Handler app/auth/mail-oauth-callback.tsx — auto-registriert
durch expo-router-File-Routing, kein Native-Rebuild (scheme 'rebreak'
schon im app.config.ts)
- mailConnectDraft-Store: pendingOAuthConnectionId für Title-Edit-Sheet
direkt nach Connect
- MailAccountCard: Password-Row hidden für OAuth-Connections, Post-Disconnect-
Modal mit MS-Account-Anleitung (DSB-konform — kompensiert fehlenden
Drittanbieter-Revoke-Endpoint mit User-Information)
Hans-Müller-DSB-Memo (mail-outlook-oauth-dsgvo-review.md):
- Section 4.1 Datenschutzerklärung-Textbaustein: "Wir widerrufen den Token
aktiv bei Microsoft"-Satz raus (war faktisch falsch — MS hat keinen
Drittanbieter-Revoke). Neuer Wortlaut: DB-Löschung + User-Anleitung
account.microsoft.com → Sicherheit → App-Berechtigungen
- Section 4.1: User.Read-Scope offen dokumentiert mit Datenminimierungs-
Klausel (Scope breiter, wir nutzen NUR Display-Name + Email-Claim)
- Section 5.1: ehrliche Doku dass MS keinen RFC-7009-Revoke hat
- Section 9 Anwalts-Themen: neue Frage 5 zur Art. 17-Erfüllung trotz
fehlendem MS-Revoke
Architektur-Eigenschaften:
- Generisches AuthMethod-Framework — Gmail/iCloud/Yahoo können später als
reine Config-Erweiterung OAuth bekommen, kein Refactor nötig
- Token-Encryption via bestehendes crypto.ts (AES-256-GCM, Key aus
Infisical)
- Consent-Gate konsistent: ConnectMailSheet-Consent-Step VOR Provider-
Auswahl (Frontend), backend-Endpoint 412 wenn consent fehlt, Daemon +
Scan-Endpoints pausieren bei consent_at=null
Open follow-ups:
- oauth_pending_states-Cleanup-Cron für abgelaufene Entries (TODO im
Backend-Code dokumentiert)
- Anwalts-Klärung Hans-Müller Section 9 (DPA-Anspruch ohne MS-Lizenz +
Art. 17 mit User-Information statt Revoke-Endpoint)
- TIA (Transfer Impact Assessment) für MS-Sub-AV — Hans-Müller-Draft-Aufgabe
- Outlook-Tile-Wieder-Aktivierung ist live, aber Phase-1-Production-Test
steht aus (User Test auf iPhone nach Pipeline-Deploy)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mail-Page-Refactor — Privacy-friendly + DiGA-tauglich:
- Custom title pro mail-connection (z.B. "Privat-Gmail" statt voller E-Mail).
Memory-Pattern: Anonymität via Nickname jetzt auch für Mail-Adressen
sichtbar, Datenminimierung. Title nullable, Fallback auf Email-Domain.
- Schema-Migration mail_connection_title (additiv, NULL default für Bestand)
- Endpoint PATCH /api/mail-connections/:id mit title-Validation (max 60,
trim, leerer String → NULL)
- "Passwort ändern"-Collapsible → vollwertige "Einstellungen"-Sektion:
Title editieren · Email read-only · Passwort neu setzen · Verbindung
trennen (mit Confirm-Dialog)
- EditMailTitleSheet als FormSheet-Pattern für Title-Edit
- mailConnectDraft-Store kriegt Title-Feld für Pre-Fill bei Re-Open
Zwei neue Stats-Charts auf der Mail-Page:
- MailBlockedByDayChart — 30-Tage-Bar-Chart, Plain-View-Bars (Pattern wie
Sparkline-Profile), Empty-State bei 0 Cooldowns
· Backend: GET /api/mail/stats/blocked-by-day?days=30
- MailDistributionChart — Half-Donut via react-native-svg, Top-5 Connections
+ "Sonstige", rendert nicht bei ≤1 Connection
· Backend: GET /api/mail/stats/blocked-by-connection
Activity-Log mit Provider-Filter:
- Filter-Chips Mo Gmail/Outlook/iCloud/etc. über bestehendem Activity-Log
- GET /api/mail/results?provider=X (war vorher hardcoded all)
- Endpoint-Naming-Fix in useMailResults (war /api/mail/blocked, jetzt
korrekt /api/mail/results — UI-Agent hatte falschen Path geraten)
Backend-Side-Effects:
- imap-providers util resolveProviderMeta(host) — gibt {provider, label,
isCustomDomain} zurück, von 3 Endpoints konsumiert
- /api/mail/status erweitert: title, provider, providerLabel,
isCustomDomain im Account-Shape
- /api/mail/results erweitert: connection-Sub-Objekt pro Entry +
provider-Filter-Query
Open follow-ups (TODOs):
- deleteOldMailBlocked-Cron löscht <24h → Bar-Chart-Daten weg. Retention
auf 90 Tage hochsetzen oder Cron stoppen.
- POST /api/mail/connect könnte die neue connection.id im Response
mitliefern → Title-PATCH direkt ohne Extra-GET (UI-Agent-Empfehlung).
- /api/mail/status zeigt nur active Connections — paused mit Title wären
unsichtbar. Entscheiden.
18 neue i18n-Keys (mail.title_*, mail.settings_*, mail.row_*,
mail.disconnect_confirm_*, mail.stats.*, mail.filter.all) in DE + EN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DSGVO Art. 9 — Compliance-Gap im Mail-Connect-Flow geschlossen (Hans-Müller-DSB
hat den Gap für Gmail/iCloud/GMX identifiziert, schon vor Outlook-OAuth-Pflicht):
- Schema: mail_connections.consent_at + consent_version + consent_ip_address;
neue consent_logs-Tabelle für Audit (grant + revoke append-only)
- Endpoints:
- POST /api/mail-connections/consent (Bulk-Array für Re-Consent, partial-fail
wirft sofort = DSGVO-sicher gegen silent-skip fremder IDs)
- POST /api/mail-connections/:id mit consent-gate (412 wenn consentVersion fehlt)
- DELETE /api/mail-connections/:id mit Widerruf-Log (OAuth-Token-Revoke als
TODO für mo Phase 2)
- GET /api/mail-connections/pending-consent — listet Bestands-Connections
mit consent_at=NULL für Re-Consent-Modal
- Account-Lösch-Bug fix: deleteAllMailConnections() war in user/delete nicht
eingebunden — Verbindungen blieben als Waisen
- Frontend:
- ConnectMailSheet: neuer Consent-Step VOR Provider-Grid (view-Machine
consent → grid → form), exakter Hans-Müller-Wortlaut für Art. 9 Abs. 2
lit. a Einwilligung
- MailConsentReminderSheet: Re-Consent-Modal beim App-Open für Bestands-User
- Stores mailConsent + mailConnectDraft (letzterer fixt Bug: Email/Provider
ging verloren wenn User Browser für App-Pw-Generierung öffnete)
- 12 neue i18n-Keys mail.consent.* in DE + EN
- Versionierter Consent-Text: art9-mail-v1-2026-05-13 (Bump bei Text-Änderung
triggert Re-Consent für alle)
Outlook-OAuth Schema (Phase 0 — additiv, Endpoints kommen später):
- mail_connections: auth_method (default 'app_password' → keine Bestands-
Connection bricht), oauth_access_token, oauth_refresh_token,
oauth_token_expiry, oauth_scope
- Encryption via bestehendes server/utils/crypto.ts (AES-256-GCM, Key aus
Infisical)
- Plan-Doc backend/docs/mail-outlook-oauth-plan.md (mo)
- DSB-Review backend/docs/mail-outlook-oauth-dsgvo-review.md (Hans-Müller):
MS als Sub-AV via DPA Sep 2025, EU Data Boundary seit Feb 2025; 5 Pflicht-
Aufgaben + Anwalts-Klärung zu DPA-Anspruch ohne MS-Lizenz
Profile — Cooldown-Pattern-Analysis als Collapsible:
- CooldownPatternAnalysis: 24h-Uhrzeit-Heatmap, Mo–So-Wochentag-Histogramm,
Top-5-Reason-Wortcloud mit Stop-Words-Filter, Cancel-Rate-Anzeige
- DiGA-relevant: NLP läuft client-side, reason-Texte verlassen das Device
nicht (gut für DSB-Akte)
- useProfileData: useCooldownHistoryFull (limit=100) für Pattern-Analyse
- Neutral formuliert, kein Stigma, alle Headings als Frage
Plan-Docs (kein Code):
- backend/docs/mail-custom-keywords-plan.md — Pro/Legend Custom-Keyword-Filter
(3.25 PT MVP, user-scoped, Body-Match in Phase 2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- mail.tsx: hide section-header "+" button when accounts.length === 0 — MailEmptyState's CTA is the sole add trigger; also replaces Pressable with TouchableOpacity
- MailEmptyState: Pressable → TouchableOpacity (no-Pressable rule)
- SheetFieldStack: add optional `intro?: ReactNode` prop rendered in a flexShrink:1 ScrollView above chips/active-input so it compresses gracefully when the keyboard is up
- ConnectMailSheet: move app-password guide + green AES block into `intro` prop so they're visible from the start, before the user types anything
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AddMacSheet + AddWindowsSheet now use FormSheet instead of the old
KeyboardAwareSheet. Steps with no TextInput disable growWithKeyboard;
Step 2 (long onboarding list) gets an internal ScrollView so content
is scrollable within the sheet cap. Sheet heights converted from fixed
px to initialHeightPct fractions.
KeyboardAwareSheet.tsx deleted — no remaining consumers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New component/KeyboardAwareScreen.tsx encapsulates the standard
KeyboardAvoidingView pattern for full-screen forms:
- iOS behavior="padding", Android no-op (adjustResize covers it)
- scrollable prop: ScrollView with keyboardShouldPersistTaps="handled"
- non-scrollable: TouchableWithoutFeedback+View for tap-to-dismiss
- headerOffset prop for screens owning their own header padding
Migrated to KeyboardAwareScreen: signin, signup, forgot-password,
confirm-otp (SafeAreaView-wrapped, no headerOffset needed) and
profile/edit (KAV wrapper only, explicit ScrollView retained).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PostCommentsSheet:
- Fix Resize-Bug: PanResponder nur auf Grabber+Header, kein onStartShouldSetPanResponderCapture
(das stahl Touch-Events von der FlatList und brach Drag-Resize)
- Height-Limits (MAX/MIN/INITIAL) als Refs in PanResponder-Closure, damit sie nicht
auf den ersten-Render-Stand eingefroren werden
- Keyboard-Show/-Hide animiert currentHeight korrekt ohne den Resize-Referenzpunkt
zu verlieren
- Avatar in CommentRow: resolveAvatar() wenn authorAvatar vorhanden, Initialen-Fallback
sonst. Bereit sobald Backend authorAvatar in Comments-Response mitliefert.
- Alle Pressable durch TouchableOpacity ersetzt
SheetFieldStack (neu):
- Progressives Multi-Input-Pattern als FormSheet-Inhalt
- Ausgefüllte Felder werden als antippbare Chips (mit Stift-Icon) nach oben verschoben
- Aktives Feld: TextInput + →/✓-Button (letztes Feld = Checkmark)
- Validate + Normalize pro Feld, Fehleranzeige unter dem Input
- suffix-Slot für Eye-Toggle etc.
- Nach letztem Feld: Keyboard.dismiss() + children (Rest des Formulars) erscheint
Migriert auf FormSheet + SheetFieldStack:
- ConnectMailSheet: Grid-View unveraendert; Form-View (email+password) via SheetFieldStack;
Zurück/Abbrechen-Header-Buttons entfernt (Schliessen = Swipe/Backdrop)
- EditMailAccountSheet: single-password-field via SheetFieldStack; Cancel-Header-Button weg
- AddDomainSheet: domain-field via SheetFieldStack; Favicon-Preview+Warning+Checkbox+Button
als children; Cancel-Header-Button weg
- CreateRoomSheet: name+description via SheetFieldStack; Public-Toggle+JoinMode+Buttons
als children; Abbrechen-Button bleibt (kein Header-Button, design-OK)
useSheetKeyboardLift: geloescht (keine Aufrufer mehr nach Migration)
KeyboardAwareSheet bleibt (AddMacSheet + AddWindowsSheet nutzen es noch)
tsc --noEmit: gruen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The custom modals each rolled their own Modal + animated-height + PanResponder +
keyboard handling, inconsistently. <FormSheet> is the single parametrized
composable, generalized from the proven PostCommentsSheet pattern:
- standard header: centred grabber + left-aligned title — NO Fertig/Abbrechen/
Zurück buttons (dismiss = swipe down / backdrop tap)
- resizable via drag on handle/header; drag-down past minHeightPct (or a fast
flick) dismisses
- height hard-capped at 75% of the screen — drag AND keyboard-expand
- keyboard-aware: sheet grows by the keyboard height (capped), iOS paddingBottom
pushes the content exactly above the keyboard; Android adjustResize handles it
- JS-driver height / native-driver translateY split (avoids the "height not
supported by native animated module" crash)
- props: title, initialHeightPct, minHeightPct, backdropOpacity, dismissOnBackdrop,
safeAreaBottom, growWithKeyboard, topRadius
Migrated (phase 1 — the no-input content sheets):
- ProtectionDetailsSheet → drops the bespoke Modal/PanResponder + the "Fertig"
header button; was 0.9–0.95 tall, now ≤0.75
- DeactivationExplainerSheet → was a pageSheet Modal with a "Zurück" button;
now the standard bottom sheet, header button gone
- PostCommentsSheet → capped its expand height 0.92 → 0.75 (TODO phase-1b: move
it onto <FormSheet> too instead of pinning magic numbers)
Phase 2 (next): <SheetFieldStack> — progressive multi-input flow (active input
pinned above the keyboard + "→" to advance, filled fields stack above, the rest
of the form reveals after the last field) for ConnectMailSheet / AddDomainSheet /
EditMailAccountSheet / CreateRoomSheet; then the auth/edit full-screen pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Privacy/stigma layer on top of the authenticated Supabase session — re-auth on
open so nobody but the user can open Rebreak. Not a login replacement.
- expo-local-authentication; NSFaceIDUsageDescription in app.config
- stores/appLock.ts: persisted `enabled` pref, in-memory `locked`, device-
capability check (`available`), device-passcode fallback on biometric failure
- AppLockGate wraps the root layout: locks immediately on `background` (not
`inactive` → app-switcher peek doesn't lock), renders LockScreen while
`enabled && locked && session`
- LockScreen: dark brand screen, auto-prompts on mount + on return from
background, "Abmelden" escape hatch (clears session → fresh login next launch)
- Settings: new "Sicherheit" section, native UISwitch; enabling requires a
successful biometric prompt first; row disabled + explained when device has no
biometrics/passcode
- de/en strings
Per product call: the lock gates the whole app incl. SOS (SOS already requires
an authenticated user, so there's no unauthenticated path to carve out).
Cold-start: appLock init blocks the splash → `locked` is set before first paint,
no flash of unlocked content. ios/ is gitignored so EAS prebuilds the new module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app/index.tsx: replaced the placeholder landing with the BrandSplash look
(#0f172a bg, SVG radial glows, breathing animation, staggered fade/bounce-ins
for app name / logo / tagline / CTAs, "Made in Germany" footer). Dropped the
"v0.1.0 RN Migration Phase 1 Skeleton" line; landing.version removed from locales.
- AddDomainSheet: onBlur runs normalizeDomain() (strips scheme/www./path/query and
email local-part) so the user sees the cleaned registrable domain before adding;
also swapped the two leftover Pressables → TouchableOpacity (no-Pressable rule).
- KeyboardAwareSheet: clamp the sheet height to (screenHeight - insets.top - 20)
while the keyboard is up, so tall sheets (e.g. AddDomainSheet's 600px) don't grow
off-screen and clip the inputs at the top.
- ConnectMailSheet: automaticallyAdjustKeyboardInsets on iOS so focused inputs scroll
into view. Covered sheets: AddDomainSheet, ConnectMailSheet, EditMailAccountSheet,
AddMacSheet, AddWindowsSheet.
JS-only (hot-reloadable).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- protection.ts: setCooldownTestMode/getCooldownTestMode (AsyncStorage 'dev:cooldown-testmode');
requestDeactivation sends testMode:true when on (__DEV__ only)
- debug.tsx: CooldownTestModeToggle (Switch) — '40s instead of 24h, staging only'
- useProtectionState.ts: wire applyCooldownDisableIfElapsed() — fires on cooldown
active→false transition (guarded so no extra fetch per poll) + on AppState 'active';
protection actually turns off when the (test-)cooldown elapses (the 'Step 5b' auto-disable)
- DeactivationExplainerSheet.tsx: useSafeAreaInsets — header paddingTop insets.top+14,
ScrollView paddingBottom max(insets.bottom,12)+24; back btn Pressable→TouchableOpacity
- ProtectionDetailsSheet.tsx: ScrollView paddingBottom max(insets.bottom,16)+24 (was 40);
backdrop + 'Fertig' Pressable→TouchableOpacity
tsc clean. (Note: 'sheet doesn't scroll' — the bottom content was being clipped under the
home indicator; the paddingBottom fix should resolve it. Broader UI polish deferred to a
separate session — Task #10.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- components/plan/PlanChangeSheet.tsx — upgrade/downgrade briefing per pricing-tiers.md §4
(fetches GET /api/plan/change-preview; gains/keeps/changes; recovery-safety line;
billing hint w/o purchase button; CTA row, no 'are you sure?' interstitial)
- debug.tsx: PlanOverrideToggle routes every flip through PlanChangeSheet first
- devices.tsx + protectedDevices.ts: 'degraded' status (red, inline 'protection expired —
remove the profile yourself' hint, no green checkmark); maxProtectedDevices limit hint
- mail.tsx + MailAccountCard.tsx + useMailStatus.ts: over-limit banner + paused-account
greyed-out + PausedBadge (all defensive — only shows if backend sends the field)
- blocker.tsx: free-tier transparency hint ('Grundschutz aktiv — voller Schutz: Pro/Legend')
+ custom-domain over-limit banner
- locales: plan.change.* + plan_limit.* (de + en)
tsc clean. Backend side (GET /api/plan/change-preview, paused/degraded fields) in progress
in parallel — UI built defensively to work before it lands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picker now uses allowsEditing:false + quality:1; picked URI routes through
AvatarCropSheet (Pinch+Pan via RNGH+Reanimated, square crop frame with
corner markers). manipulateAsync crop left as TODO — expo-image-manipulator
not yet installed; sheet passes URI through unchanged until then.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Alle <Pressable style={({pressed}) => ({...})}> ersetzt — style-Funktion
droppt auf Android (New Arch) intermittierend width/height, führt zu 0×0
unsichtbaren Elementen. TouchableOpacity mit activeOpacity ist stabil.
Außerdem übrige Pressables (plain style) aus components/ und app/
migriert sowie zwei überschüssige </View>-Tags in chat.tsx + RoomCard.tsx
entfernt die TS-Fehler verursacht haben.
64 Dateien, typecheck sauber.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- protection.ts: normalize Android device-state keys (vpn/accessibility/
tamperLock) to the iOS-shaped names the UI reads (urlFilter/familyControls/
appDeletionLock) — on Android the layers came back under different keys, so
blocker.tsx saw all toggles as undefined → always off → optimistic toggle
flipped back to off after enabling
- AppHeader.tsx: avatar/bell/back Pressable-with-style-fn → TouchableOpacity
with plain style — style-fn was swallowing width/height on Android → 0×0
+ overflow:hidden → avatar invisible (same pattern as Mac-CTA fix 7d04e42)
- app.config.ts: adaptiveIcon.foregroundImage → padded adaptive-foreground.png
(logo in ~66% safe zone, was full-bleed → clipped by launcher mask);
icon → icon.png (clean 1024 opaque, was the 512px alpha variant)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- upsertMailConnection: bei Update lastConnectError + lastConnectErrorAt auf
null — User aktualisiert App-Passwort → UI zeigt sofort wieder Live (statt
stale Auth-Fehler-Status bis nächstem IDLE/Scan-Cycle)
- /api/mail/status: liefert lastConnectError, lastConnectErrorAt,
lastIdleHeartbeatAt mit (waren bisher nicht im Response → Frontend hat den
Status nie korrekt rendern können)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend wirft 403 device_limit_reached für ALLE auth'd endpoints sobald User über
plan-limit ist. Bisheriges Frontend hat silent gefailt → Profile/Notifications/etc
zeigten nichts mehr, User war verwirrt.
Now:
- lib/api.ts: 403 device_limit_reached intercepten, parse error.data.devices,
trigger useDeviceLimitStore.show()
- stores/deviceLimit.ts: Zustand store (visible, devices, max, plan, show/hide)
- components/DeviceLimitReachedSheet.tsx: TrueSheet (UISheetPresentationController)
Auto-präsentiert wenn store visible, zeigt device-list mit trash-button per Eintrag,
DELETE /api/devices/:id mit skipDeviceHeader: true (sonst circular 403)
- app/_layout.tsx: <DeviceLimitReachedSheet /> als globaler overlay vor <Stack>
- i18n: device_limit_* keys DE+EN
UX: User sieht jetzt sofort native bottom-sheet mit erklärung + actionable
device-list statt silent fail. Auto-close wenn devices.length < max nach delete.
TS-fix: detents={['auto', 1] satisfies SheetDetent[]}, onDidDismiss statt onDismiss
(prop heißt anders in TrueSheet API).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stores/language.ts:
- init() override AsyncStorage-Wert nicht — wenn nichts gespeichert,
i18n bleibt bei deviceLocale (von lib/i18n.ts via Localization.getLocales).
Vorher: forced 'en' default obwohl App auf DE.
ComposeCard share-button:
- borderRadius:12 + height:50 → rounded-full px-5 h-11 (44pt)
- text-base → text-sm. Pill-Pattern wie Pre-Session.
app/profile/index.tsx:
- AppHeader title "Profil" → "Profil DEBUG-2300" — TEMPORARY marker
zur Verifikation ob File geladen wird (user-suspect: routing zu altem
File). Wird nach Test wieder entfernt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnose: ComposeCard ist ListHeaderComponent der FlatList die
keyboardShouldPersistTaps="handled" hat. Aber das Prop propagiert nicht
rekursiv durch tiefere View-Ketten — der Share-Button-Pressable liegt
zu tief, der erste Tap dismissed nur die Tastatur.
Fix: onPress → onPressIn (fired beim Touch-Down vor Keyboard-Dismiss-
Logic). Identisch zum Pattern beim Bild-Icon. Guard if(!content.trim()
|| posting) repliziert die disabled-Logik die onPress hatte.
Style: native iOS Primary-Action-Button-Look:
- height 44→50, px-5→px-6
- rounded-full → borderRadius 12 (Rectangle statt Pill)
- Nunito_700Bold → 600SemiBold
- Plain text "Teilen", kein Icon
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ComposeCard:
- paper-plane-Icon entfernt, h:52→44, px-6→5, layout flex-row gap-2 →
items-center justify-center, only Text. Saubere Pill ohne Icon-Gewicht.
- i18n-Key community.share war schon da — kein neuer Key.
HeaderDropdownMenu:
- minWidth 170→210, numberOfLines={1} auf Item-Labels entfernt (Breite
reicht jetzt fuer alle Labels). numberOfLines bei SOS-Block-Labels
bleibt (zwei separate Text-Nodes, sinnvoll).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(app)/index.tsx:
- FlatList keyboardShouldPersistTaps="handled" — Bild-Icon im ComposeCard
reagiert ab erstem Tap auch wenn Tastatur offen. Vorher dismisste der
Tap nur die Tastatur (RN-Default "never").
ComposeCard.tsx Teilen-Button:
- height 44→52, px-5→px-6, paper-plane-outline-Icon size 18 + text-base
Nunito_700Bold. Standard-iOS-Filled-Primary-Button-Style.
AppHeader.tsx Bell + Avatar:
- hitSlop 4pt allseitig auf beiden Pressables — effective tap-area
36→44pt ohne Layout-Verschiebung
- Bell-Icon size 18→22 (konsistent mit Avatar-36pt-Kreis)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Profile:
- Hint "fuelle deine anonymen Daten aus" oeffnet das DemographicsAccordion
jetzt automatisch via expanded-Prop + useEffect mit LayoutAnimation.
Vorher: scrollte hin, liess es geschlossen, User musste nochmal tappen.
- DemographicsAccordion: expanded-Prop fuer external-trigger; interner
expandedLocal-State, Toggle-Button bleibt unabhaengig functional.
ProtectionDetailsSheet FAQ:
- chevron-forward (0deg→90deg Rotation, sah aus wie Nav-Link) → chevron-down
(0deg→180deg). Geschlossen=runter, offen=hoch. State-Toggling war schon
korrekt, nur visuelle Affordance war falsch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- urge.tsx: TtsProviderToggle + LlmProviderToggle entfernt (Testing durch);
Core-Logic (useTtsProvider, currentLlmProvider, BenchSession) bleibt für
spätere Debug-Page intakt
- DemographicsAccordion FieldRow: flex:1 auf Label-Text, kein flexShrink-
Wrapper mehr nötig; Label+Input wrappen nicht mehr auf schmalen Devices
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>