## Protection Pre-Explainer: External Pointer
Vorher: Pulse-Ring absolute-positioniert IM Screenshot — Position musste
per-locale fine-tuned werden weil Apple-Dialog-Höhe variiert (DE/EN/FR/AR
haben unterschiedliche Text-Längen → Dialog hat verschiedene Höhen →
Erlauben-Button rutscht).
Jetzt: animierter Pfeil + Label-Pill UNTER dem Screenshot. Dimensions-
agnostic, funktioniert in allen 4 Sprachen ohne Locale-spezifische Magie.
- ScreenshotPointer komplett refactored: caret-up + bouncing pill mit
Button-Label-Text (z.B. 'Tippe "Erlauben"' / 'Tap "Allow"' / etc.)
- onboardingAssets.ts: getPointerPosition deprecated/entfernt
- ProtectionSlide nutzt neue API mit buttonLabelKey
- 4 Locales: dialog_button_allow + dialog_button_continue
- tap_marker_hint refined (kein "roter Marker"-Ref mehr)
## i18n-aware Screenshots
en/fr/ar Permission-Dialog-Screenshots zur Map ergänzt. Resolver fällt
auf de zurück wenn andere Sprache fehlt.
## Dynamic Sizing
ProtectionSlide nutzt useWindowDimensions:
height: min(320, max(200, screenH * 0.32))
→ passt auf iPhone SE (213px) bis Pro Max (320px capped) ohne Scroll.
OnboardingShell ScrollView-Padding reduziert (16→12 top, 24→16 bottom).
ProtectionSlide-Spacing tightened.
## Blocker: lockedIn Fix
Bug: `lockedIn = appDeletionLockActive` ignorierte URL-Filter-State —
wenn User nur FC aktivierte (ohne URL-Filter), zeigte App grünen "Schutz
aktiv"-Banner obwohl URL-Filter aus war. Fix:
lockedIn = urlFilter && appDeletionLock
→ Beide müssen wirklich aktiv sein für den grünen Banner.
## LayerSwitchCard: lockedHint Prop
Optional Hint-Text der unter dem active Layer angezeigt wird, z.B.
"System-gesperrt. Nur in iOS-Einstellungen → Bildschirmzeit → Verwaltung
durch ReBreak deaktivierbar.". Wird für iOS App-Lock-Card genutzt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related fixes after the user saw a raw 400 JSON dump in the sheet
("API 400: { error: true, message: 'Eintrag bereits vorhanden' … }").
1. apiFetch now extracts the prettiest available message from the
response body (data.message → message → statusMessage → raw text →
bare status code) and throws an Error whose .message is that string
only. Stashes the structured pieces on the Error too (.code, .data,
.status) so callers that switch on error codes still have them, but
the default `e?.message` path delivers a clean human sentence.
2. AddDomainSheet maps the known error codes to localized strings —
WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED / INVALID_MAIL_DOMAIN /
DISPLAY_NAME_NOT_SUPPORTED / INVALID_DOMAIN / "Eintrag bereits
vorhanden" (duplicate) — and falls back to a generic copy if the
code is unknown. The raw API JSON never appears in the UI again.
Plus the kind-override checkbox: the auto-detect (input contains "@" →
mail, contains "." → web) is fine for the typical case but a user can
type a clean domain and still want it filtered against mail senders
(e.g. they know "casino.de" is also their casino's sender domain).
The new pill below the preview toggles between mail and web, defaults
to whatever auto-detect said, and resets when the input is cleared. The
local-part strip still runs for mail-mode so the stored value stays a
domain.
i18n: error_invalid_mail / error_invalid_input / error_duplicate /
kind_override_label across DE/EN/FR.
User added info@info.mail-slotoro.com and it landed in Eigene Domains
as type=web instead of in Eigene Mails as type=mail_domain. Bug trace:
1. AddDomainSheet detects kind='mail' from the @ in the user's input
2. mailDomain() strips the local-part → "info.mail-slotoro.com"
3. handleAdd calls onAdd(pattern) — only the stripped string, no kind
4. useCustomDomains.addDomain then sends { pattern } with no kind
5. Backend Variante C auto-detect keys on @ in the pattern — but the
pattern no longer contains @ (frontend already stripped it), so the
detector falls into the kind='web' branch
Fix: pass the kind explicitly from the sheet through the prop chain.
AddDomainSheet.onAdd is now (pattern, kind?) — the sheet's handleAdd
forwards the kind it detected. blocker.tsx's onAdd handler threads
it into addDomain so the body includes { pattern, kind }. Backend
then takes the explicit path and stores type='mail_domain' for the
already-stripped value. Auto-detect on bare pattern (no kind) still
works for any caller that genuinely doesn't know — that path just
isn't used by the sheet anymore.
Single shared affordance for adding either a website-domain or a mail-
sender-domain. The per-section add buttons (one inside "Eigene Domains"
and one inside "Eigene Mails") are gone — replaced by a CustomFilter-
Overview card above both sections with:
- title "Eigene Filter" and a "X von 20" counter (free/pro: 10, legend:
20 — sum of the two per-type buckets)
- a 2-colour progress pill: brandOrange for the web slice, success-green
for the mail slice on top of the surface-elevated rest
- a 48×48 rounded-full TouchableOpacity on the right (brandOrange,
ionicons add 24px, white) that opens the AddDomainSheet directly
AddDomainSheet was rewritten one more time: the Seite / E-Mail type
picker is gone. The user types one thing — domain or full address —
and a live preview shows which one we detected (Domain-Filter for a
bare host, Mail-Filter for input that contains "@", stripping to the
domain after the last @). The shape is also what we send: the body is
{ pattern } with no kind field. The backend (commit a2680f6) does the
authoritative auto-detect and sends back the resolved type with the
created row, so the frontend never has to guess in two places.
useCustomDomains.addDomain now treats kind as optional. When omitted,
the request body just carries pattern — when present it's still sent
through verbatim so any caller that wants to force a category still can.
DomainSection no longer renders a per-section add button when its onAdd
prop is undefined — domains and mails sections in blocker.tsx both
omit onAdd now. The mails section stays default-collapsed.
i18n: new keys custom_filter_overview_title / count + preview_web /
preview_mail / preview_invalid; tabs_web / tabs_mail removed since the
TypePicker is gone. type_web / type_mail kept in the locales as
inactive entries in case the type-picker comes back in a future
direct-add flow.
The Seiten/Mails top-tabs added in 5c6fa3d are gone. Per the user's
revised vision, web-domains and mail-patterns live side by side as two
collapsible <DomainSection>s with their own header, slot pill, progress
bar, and add-button — closer to the original Eigene-Domains affordance
plus a sibling Eigene-Mails section. Both default open; chevron-up/down
per the existing icon convention.
AddDomainSheet was rewritten from scratch to fix the layout-bug
visible in the screenshot — SheetFieldStack's two-ScrollView intro/
fields split was wrong for a single-input use case and was rendering
the chip at the bottom of the scroll area with a huge gap under the
TypePicker. The new sheet is a plain ScrollView with TypePicker, label,
TextInput, help-card, preview-card, warning-card, confirm-row, and the
Cancel + Hinzufügen buttons stacked top-to-bottom with `gap: 12`. No
Pressable anywhere — TouchableOpacity only, per the hard rule.
DomainGrid is now a pure tile renderer: the header / slot pill / add
affordance live on the section component above it. Its `kind` prop
(renamed from `activeTab`) drives the type filter — for v1.0, mail
means strictly `mail_domain` (display-name is gone).
i18n: new keys section_domains / section_mails / add_sheet_cta. mail-
related copy (label, placeholder, help, empty) had every "Display-Name"
mention stripped so the user can't read about an option that doesn't
ship.
Progressbar inline in DomainSection with the same Animated.timing
pattern DeviceProgressBar uses, with a 3-step color threshold
(green / brandOrange / error) keyed on the bucket fill ratio.
Two bugs reported on the new mail-pattern flow:
1. The sheet sent the full local@domain.tld pattern to the backend so a
user blocking communications@only4-subscribers.com would only catch
that exact local-part — newsletter@, info@, promo@ from the same
sender would slip through. Casino affiliates rotate the local-part
on every blast while keeping the domain stable, so we now strip the
local-part on submit. The preview-card under the input shows what
actually gets stored (only4-subscribers.com), so the user sees the
pattern that will hit. Bare tokens without "@" stay as-is and reach
the backend as display-name candidates.
2. FormSheet's backdrop was a <Pressable> — straight violation of the
"TouchableOpacity, never Pressable" rule. Swapped for
<TouchableOpacity activeOpacity={1}> so the tap-to-dismiss still
works with no visible feedback on the dim layer.
Top-tabs above the custom-domains grid: Seiten (web) and Mails (mail_*).
2px underline highlight in colors.brandOrange for the active tab, the
muted label otherwise — matches the community/feed tab style we already
use. Pill segmented control would have needed extra inset math for two
tabs without adding clarity.
- DomainGrid filters items by the active tab. Tab-specific empty-state
copy and icon (mail-outline for the Mails tab) so the empty Mails tab
doesn't read like a broken Web view.
- mail_display_name tiles hide the submit-to-global button entirely —
matches the v1.0 backend lock; the user can't accidentally tap into a
400 from the API.
- useCustomDomains exposes countsByType + limits. Provisional client-
side estimation until the new API response shape (extended in the
parallel backend commit f2b81ee) is wired through — same TS shape,
so dropping the estimation is a one-line swap when ready.
- AddDomainSheet picks up initialType so tapping "+" while the Mails tab
is active opens the sheet pre-selected to E-Mail. Plan-limit error
handling maps WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED to the right
per-bucket message.
i18n: tabs_web / tabs_mail / count_label / error_web_limit_reached /
error_mail_limit_reached / empty_web / empty_mail across DE/EN/FR with
%{var} placeholders.
AddDomainSheet now opens with a Seite / E-Mail segmented control.
Web keeps the existing flow (label, placeholder, favicon preview,
domain normalization). Mail switches to a free-form pattern input
(address / domain / display-name — user types what they see in
their inbox) with a mail-icon preview after the field is filled.
addDomain(pattern, kind) now sends { pattern, kind: 'web' | 'mail' }
and the server decides the concrete type. Type field flows through
the CustomDomain type so DomainGrid tiles render the mail-outline
icon for mail entries instead of the favicon fallback.
i18n: blocker.type_web / type_mail / add_web_* / add_mail_* across
de/en/fr with %{var} placeholders per repo convention.
User-Feedback nach Live-Test:
Frontend (mail page):
- HalfDonut als shared component in components/common/HalfDonut.tsx
extrahiert (vorher local in ProtectionDetailsSheet). Mail-Page nutzt
jetzt dieselbe SVG-Math, Animation und Stroke-Style wie der
Blocker-Schutz-Details-Sheet — visuelle Konsistenz auf einen Blick.
Mail-Donut: width=168 (kompakter als die 220 in Blocker, weil Legend
rechts daneben sitzt).
- Donut zeigt Total in der Mitte mit kompaktem Format:
< 1000 → "999", >=1000 → "1.2k+" / "12k+" / "27k+"
Headline-Zahl oben links entfällt — Total ist im Donut-Center.
- "Mehr Infos" + "Kürzlich blockiert" zu EINER Top-Level-Collapsible
zusammengefasst. Beim Aufklappen: Bar-Chart direkt sichtbar, nested
Collapsible "Kürzlich blockiert" darunter (default zu).
- Account-Card Expanded: per-Connection-Bar-Chart mit adaptive
Granularität nach Connection-Age:
· <24h → Empty-State "Daten werden gesammelt, Auswertung nach 24h"
· 1-14d → Day-Buckets (echte Daten via /api/mail/stats/blocked-by-day
?connectionId=)
· 15-90d → Week-Buckets (client-aggregiert)
· >90d → Month-Buckets (client-aggregiert)
- Settings-Sheet komplett refactored: State-Machine `mode: 'list' |
'edit-title' | 'edit-email' | 'edit-password'` mit Back-Pfeil. Inline-
Edit im selben Sheet statt Sub-Sheet öffnen (FormSheet-Pattern).
Email-Edit-Row vorbereitet (Backend-PATCH-Endpoint kommt separat).
- Pen-Icons app-weit entfernt: SheetFieldStack-Row, alle Settings-Rows
auf chevron-forward (Memory-Konvention).
Frontend (MailAccountCard status fix):
- resolveStatusDot nutzt jetzt heartbeat-as-fallback. Vorher: "waiting"
wenn lastScannedAt=null, egal ob Daemon längst connected war. Jetzt:
"waiting" nur wenn weder lebendiger Heartbeat noch vergangener Scan
existiert → frisch verbundene Connections (z.B. OAuth-Outlook 5s nach
Connect) zeigen direkt "live".
- Behebt User-Beobachtung: "wartet auf erste verbindung" bei Outlook
obwohl Daemon-Log "connected, auth=xoauth2" zeigt.
Backend (imap-idle daemon):
- getMailboxLock("INBOX") jetzt mit 30s Promise.race-Timeout gewrappt.
- Outlook/XOAUTH2 hat den Edge-Case, dass der Mailbox-Lock lautlos
hängt nach erfolgreichem connect — die Session bleibt offen ohne
Fortschritt bis der Renew-Timer (10min) ein imap.close() schickt.
Mit Timeout wird das Failure-Mode explizit → Auth-Retry-Loop greift
sauber + last_connect_error mit klarem Text (statt stiller Hänger).
- Root-Cause "warum hängt es" noch nicht behoben — Diagnose nach
Deploy in Logs (mo).
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
- 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>
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>
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>