Backend (voice-call groundwork, no call engine yet):
- Profile.callsEnabled (Boolean default true) + migration
- canCall(caller,callee): mutual-follow AND callee.callsEnabled — server-side hard guard
- POST /api/me/calls-enabled (opt-out toggle), GET /api/chat/can-call/:userId
- expose callsEnabled in /api/auth/me
Frontend:
- "Allow calls" toggle in Profile privacy section (default on, optimistic+rollback)
- Me.callsEnabled + i18n DE/EN/FR/AR
Bundled DM UI work from this session:
- image lightbox is now a swipeable carousel over all shared images (+ counter)
- keyboard stays open after sending (input ref refocus)
- voice notes: Instagram-style waveforms (own=white/mint, other=black/grey),
removed the blue progress dot; lazy-load expo-media-library with clean fallback
- expo-linear-gradient + expo-media-library deps
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Magic-Mac-Hub (/api/magic/devices):
- Filter boundToPlan war zu eng \u2014 iPhone/iPad ohne aktiven Plan-Lock
fielen raus. Jetzt: alle UserDevice-Rows des Users ausser den
magic-enrolled, plus ProtectedDevice mit Dedupe.
Native /devices Page:
- MacBook erschien doppelt: einmal als UserDevice (registriert via
Magic-Mac, model=Mac14,9) und einmal als ProtectedDevice (alter
DNS-Flow). Dedupe per platform-key (mac/ios/android/win):
wenn UserDevice mit gleicher Plattform existiert, blende
ProtectedDevice aus.
- Slot-Counter zaehlt jetzt nach dedupe (totalRegistered).
serverAssets approach didn't bundle the template into the Nitro
output (no .output-staging/server/chunks/raw/ dir, no asset-storage
mount in nitro.mjs). Logs confirm: '[Magic] Profile template missing
in serverAssets'.
Drop serverAssets entirely. Inline the template (~2KB) as a TS
constant in backend/server/utils/magic-profile-template.ts. Build-
robust, no FS/storage dependency at runtime. Canonical source of
truth remains ops/mdm/rebreak-mac-dns-filter.mobileconfig — keep in
sync manually until/unless we add a codegen step.
- previewNode: add audio case → VoiceNoteBubble renders correctly in blur
overlay when long-pressing a voice message (was rendering empty/null)
- MessageActionMenu: account for input bar (bottomInset=90) in positioning —
menu no longer slides behind input bar on bottom messages; barTop clamped
to never overlap the menu; both above/below paths respect usableH
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
iCloud-Sign-In Pattern: wenn ein neues Gerät versucht sich anzumelden
und das Plan-Limit erreicht ist, kann der User auf einem bereits
angemeldeten Gerät bestätigen — Code wird auf BEIDEN Geräten gezeigt
für visuellen Vergleich (verhindert Code-Forwarding-Attacken).
Backend:
- New table device_approval_requests + supabase_realtime + RLS
- POST /api/devices/approvals — create (new device)
- GET /api/devices/approvals — list pending (existing devices)
- GET /api/devices/approvals/:id — status poll (new device)
- POST /api/devices/approvals/:id/approve — approve + atomic evict
- POST /api/devices/approvals/:id/reject — reject
- POST /api/devices/approvals/:id/email — trigger email fallback
- POST /api/devices/approvals/email/:token — magic-link approve (no auth)
- Email-Template via Resend (lyra-neutral, security-formal)
- 10min TTL, 6-digit numeric codes (crypto-random)
Frontend (rebreak-native):
- DeviceApprovalIncomingSheet — existing devices: code + device-picker + Allow/Reject
- DeviceApprovalPendingSheet — new device: code + spinner + 'Send via email'
- useDeviceApprovalRealtime — postgres_changes subscription
- DeviceLimitReachedSheet — neues CTA 'Auf anderem Gerät bestätigen'
- i18n DE/EN/FR/AR
Migration läuft automatisch via prisma migrate deploy bei push.
- MessageActionMenu: scharfe Preview-Kopie der gedrückten Bubble am Anker
(bleibt über dem Blur sichtbar, WhatsApp-Stil) statt mitgeblurrt
- Reaktions-Leiste: kein Ring/Hintergrund mehr, aktives Emoji nur leicht größer
- Reaction-Pills: plain Emoji + Count ohne Hintergrund/Border
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In der "Vordefinierte Top-Seiten"-Sektion der VIP-Liste ein
"Seite vorschlagen"-Link → SuggestCuratedSheet: Domain-Eingabe →
POST /api/curated-domains/suggest (Land via Geräte-Region). Response-
Handling: Erfolg / schon vorgeschlagen / approved / rejected / ungültig.
- useCuratedSuggest.ts (neu), SuggestCuratedSheet.tsx (neu)
- VipDomainList.tsx: Suggest-Link in der curated Sub-Sektion + Sheet-State
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Der setTimeout(80)+onImageLoad-Ansatz war ein Timing-Hack gegen ein
strukturelles Problem (lazy Item-Measurement unter Fabric -> scrollToEnd
landet zu kurz). Stattdessen jetzt inverted FlatList: Index 0 sitzt
permanent am Bildschirmrand, neueste Nachricht immer sichtbar.
- dm.tsx: inverted + reversedMessages, Gruppen-Logik gespiegelt,
manuellen Auto-Scroll + keyboardHeight-State entfernt
- ChatBubble.tsx: onImageLoad-Prop entfernt (obsolet)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- useCustomDomains: CustomDomain um vipDeferUntil/vipEvictAt, AddDomainResult
um vipFull/newDomainId; addDomain liefert vipFull durch; submitVipSwap()
- VipSwapSheet (neu): Dialog wenn VIP voll — User wählt eine eigene Domain,
die in 24h ersetzt wird
- VipDomainList: Badge „wird in Xh ersetzt" auf der ersetzten Kachel
- blocker.tsx: vipFull → AddDomainSheet zu, VipSwapSheet auf
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug 1 — Chat-Liste: RefreshControl nutzte React-Querys `isRefetching`,
das bei JEDEM Background-Refetch (focus-/stale-getriggert) true wird →
nach Zurück-Navigation hing der Pull-to-Refresh-Spinner endlos. Fix:
eigener `userRefreshing`-State, nur bei explizitem Pull-to-Refresh true,
im finally zurückgesetzt.
Bug 2 — Conversation scrollte nicht bis zur letzten Nachricht, wenn die
ein Bild war: onContentSizeChange-scrollToEnd feuerte vor dem Bild-Load.
Fix: ChatBubble bekommt onImageLoad-Callback, die letzte Bild-Nachricht
triggert nach dem Laden erneut scrollToBottom.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PostCard: Bilder mit borderRadius 10 + overflow:hidden — Ecken wieder rund
- dm.tsx: myUserId synchron aus useAuthStore statt async getSession —
behebt den hängenden Lade-Spinner beim Zurück aus einer Conversation
(async getSession-Fenster auf jedem Mount → enabled-Flackern der Query)
- ChatBubble: expo-image memory-disk-Cache + 200ms-Transition für
smootheres Bild-Laden
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Custom-Domain-Slots sind jetzt EIN gemeinsamer Pool für web + mail
(Pro 10 / Legend 20) statt getrennter web/mail-Buckets. Free-Tier ist
entfallen — PLAN_LIMITS hat nur noch pro + legend, getPlanLimits
defaultet auf pro.
Backend:
- plan-features: customDomains ist eine Zahl (CustomDomainLimits weg)
- index.post: Slot-Check gegen Gesamt-Count, Fehler einheitlich LIMIT_REACHED
- index.get: liefert { items, count, limit }
- change-preview + coach/message an die neue Form angepasst
Frontend:
- useCustomDomains: count/limit (Zahlen) statt countsByType/limits
- AddDomainSheet: ein generischer Limit-Hinweis (error_limit_reached)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
VIP-Liste-Sektion: zwei Kachel-Sektionen statt flacher Chips —
"Meine VIP-Domains" (eigene Custom-Domains, Stern + Status-Badge) und
"Vordefinierte Top-Seiten" (kuratiert, schlicht). Read-only, kein
Freigabe-Button. Kein Pulse-Ring (auf User-Wunsch entfernt).
docs/concepts/brand-token-matching.md: abgenommenes Konzept für geteiltes
Brand-Token-Matching (Layer 1 DNS + Mail/Mo) gegen den Nummern-Trick der
Gambling-Industrie (slotoro.bet → slotoro88.bet). Im Backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Blocker-UI:
- FilterTile: Trash-Button → status-aware Freigabe-Button (Freigeben/Erneut/
in-Prüfung); RemoveDomainSheet entfernt — kein Domain-Entfernen mehr in der UI
- VIP-Liste landabhängig: zeigt die komponierte Endpoint-Liste statt nur
eigener Customs; Land über Geräte-Region (expo-localization)
- VIP-Realtime: refetch bei Domain-Add/Approve/Reject, pulsierender Ring
für neue/active/submitted Chips
VIP-Komposition (webcontent-domains):
- Hybrid: Customs auf 30 gekappt, 20 Slots fest für die kuratierte Top-Liste
reserviert — Customs können die Top-Gambling-Domains nicht verdrängen
Add-Check (custom-domains POST), für web reaktiviert — 3 Fälle gegen
Layer 1 (global) + Layer 2 (kuratierte VIP):
- weder global noch kuratiert → normaler active-Eintrag
- global + kuratiert → alreadyProtected, kein Slot
- global, nicht kuratiert → inGlobalNotVip; per addToVip als status=approved
speicherbar (kein Slot, nur VIP-Liste)
DE-Gambling-Liste 30→36, nach Relevanz sortiert (erste 20 = reservierte Plätze)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Meine Filter (unified web + mail_domain, Typ-Badge pro Kachel, Slot-Pool-
Zaehler) + collapsible VIP-Liste (Zweitschutz-Beschreibung, read-only). Die
3 redundanten Web-Bloecke (CustomFilterOverview + DomainSection Eigene Domains)
entfernt. Slot-Pool rein frontend (limits.web+limits.mail), kein Backend noetig.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Neue VIP-Sektion in der Blocker-Page: eigene Web-Custom-Domains als Tiles mit
Zaehler (X/10 Pro, X/20 Legend), Hinzufuegen frei. Entfernen geht durch einen
3-Klick-Friction-Flow (RemoveDomainSheet, gespiegelt vom Deactivation-Muster) —
kein Block-Abbau im Craving-Moment. free-Tier-Zweig entfernt (kein Free mehr).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
debug.tsx war hart __DEV__-gated → der Protection-Log-Viewer (v0.3.3)
wäre im TestFlight-Build unerreichbar gewesen.
eas.json production-Profil setzt jetzt EXPO_PUBLIC_ENABLE_DEBUG=1.
debug.tsx + HeaderDropdownMenu prüfen `__DEV__ || EXPO_PUBLIC_ENABLE_DEBUG`.
Für den echten App-Store-Release einfach das Flag aus eas.json nehmen.
Avatare (Dicebear-URLs), Chat-Attachments und Feed-Bilder wurden bei
jedem App-Reload neu vom Netzwerk geladen — RN Image hat nur flüchtigen
Memory-Cache. expo-image (~3.0.11) bringt persistenten Disk-Cache
(cachePolicy 'memory-disk' default).
14 Files migriert: UserAvatar, ChatBubble, RoomCard, ChatInput, PostCard,
ComposeCard, NotificationsDropdown, AppHeader, ProfileHeader,
AddDomainSheet, DomainGrid, room, profile/edit, signup.
API-Mapping: resizeMode→contentFit. PostCard onLoad las e.nativeEvent.
source — expo-image liefert e.source direkt (sonst wäre der Post-Bild-
Aspect-Ratio-Fix still gebrochen).
PostCard: nur Image-Zeilen angefasst, Like/Count/Memo-Logik unberührt
(memory/feedback_minimal_post_changes.md).
Kommt mit v0.3.3 (expo-image ist Native-Modul, braucht neuen Build).
Back-Button:
- OnboardingNavContext liefert der Shell einen optionalen goBack-Handler
(kein prop-drilling durch 8 Slides).
- OnboardingShell: chevron-back links neben der Progress-Bar wenn goBack
gesetzt ist.
- Controller: goToLinearPrevious() + BACK_ALLOWED-Liste. Back nur auf
privacy/nickname/diga_choice/plan/payment — NICHT welcome (erste),
done (final), diga_code (eigener onBack), protection (Backend-Step +
Permission-Flow).
Language-Switcher:
- WelcomeSlide: 4 Sprach-Pills (DE/EN/FR/AR) oben rechts. User kommt
während Onboarding nicht zu Settings — sonst kein Weg die Sprache
zu wechseln. setLanguage persistiert + flippt RTL für AR.
Bug: User mit FR-locale sahen Lyra-Confirmation-Posts trotzdem auf Deutsch
(Banner/Tabs richtig FR). Root: approve.post.ts generierte den Text via
Groq mit hartcodiertem 'auf Deutsch'-Prompt, speicherte als plain content.
Server (approve.post.ts):
- 4 parallele Groq-Calls (Promise.allSettled) — de + en + fr + ar
- Per-Locale-PROMPT_CFG mit subject/action/statsLine/thanksSegment-Texten
- Locale-aware Number-Format (toLocaleString('de-DE'|'en-US'|'fr-FR'|'ar-EG'))
- Content als JSON {de:'...',en:'...',fr:'...',ar:'...'} gespeichert
- Mindestens DE muss gelingen, sonst kein Post (Sicherheit gegen halbe Posts)
- ~4x Groq-cost pro Post (sehr günstig bei Llama-3.3-70b, parallel-latency
bleibt ähnlich)
Frontend (PostCard.tsx):
- resolveLocalizedJsonContent() — try-parsed JSON content
- Wenn JSON-Object mit Locale-Keys → pickt i18n.language, fällt auf DE → EN
- Sonst plain content (Legacy-Posts, Comments, User-Posts unverändert)
- Quick-Reject auf '{' first-char vermeidet JSON.parse-Overhead für 99.9%
der Text-Posts
Legacy-Posts in DB bleiben DE-only (kein retroaktiver Multi-Locale-Rewrite).
Neue Posts ab Deploy haben alle 4 Sprachen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
## TTS Auto-Play Preference
User-Request: wenn Voice einmal aktiviert, soll Lyra auf jeder Slide
automatisch sprechen — nicht jede Slide extra antippen.
- stores/lyraVoice.ts: zustand-store mit AsyncStorage-Persistence
(@rebreak/lyraVoiceEnabled). Default OFF.
- LyraBubble auto-plays on text-change wenn enabled
- Audio-Button toggled die Preference + stoppt current playback
- Visuell: Button ist orange-filled wenn voice ON, ghost-bordered wenn OFF
- Icon: volume-mute-outline (OFF) / volume-medium / hourglass / stop
- Cleanup beim Unmount (stopLyraSpeech) + bei text-change
Initialisiert via init() in app/_layout.tsx (analog language/theme/appLock).
Locale-keys: audio_play → "Stimme einschalten", neu audio_disable → "Stimme
ausschalten" in 4 Sprachen.
## DiGA Test Codes 011-100
Aktuell 10 Codes (REBREAK-TEST-001..010), aber 100 Android-Tester kommen
morgen onboarding. Migration 20260518_extend_diga_test_codes seeded 90
zusätzliche Codes via generate_series(11, 100) + LPAD-Padding.
- Label: 'test_batch_2026-05-android' für Auditbarkeit (vs '...2026-05'
für die ersten 10)
- grants_plan: 'legend' wie die ersten 10
- ON CONFLICT DO NOTHING — idempotent
Distribution-Pattern: Tester N kriegt Code REBREAK-TEST-<NNN-padded>.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: 3 Stellen hatten `behavior={Platform.OS === 'ios' ? 'padding' : undefined}`.
Auf Android = `undefined` = KeyboardAvoidingView macht NICHTS → Input wird von
Tastatur verdeckt (chat-input, profile-edit-nickname, room-chat).
Fix: switch zu react-native-keyboard-controller's KeyboardAvoidingView mit
behavior='padding' für beide Plattformen. Funktioniert sauber cross-platform
weil KeyboardProvider schon im root-layout sitzt.
Affected Files:
- components/KeyboardAwareScreen.tsx (used by profile-edit + auth-screens)
- app/dm.tsx (DM chat)
- app/room.tsx (room chat)
lyra.tsx war bereits OK (`'height'` für Android — kein Fix nötig).
iOS-Verhalten unverändert.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
## 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>
## Backend: Anti-Auto-Reactivation nach Cooldown
Bug: nach Cooldown-Ablauf wurde der URL-Filter automatisch wieder
reaktiviert (enforceProtection-Loop fängt 'recoveringFromBypass'-Phase ab).
Damit war der Cooldown-Schritt entwertet — User konnte nicht wirklich
abschalten, weil die App den Schutz sofort wieder hochfuhr.
Fix: Profile.protectionDisabledAt (DateTime nullable). Wird in
/api/cooldown/status auf cooldown-auto-resolve gesetzt. /api/protection/state
gibt dann protectionShouldBeActive=false zurück → Frontend macht KEINE
Auto-Reactivation. User muss explizit re-aktivieren (CTA in der App).
- Migration 20260517_protection_disabled_at
- Schema: Profile.protectionDisabledAt
- /api/cooldown/status: setzt das Feld auf expired+resolve
- /api/protection/state: includes profile.protectionDisabledAt in shouldBeActive-Berechnung
- /api/protection/mark-active (POST, NEU): clears das Feld, vom Frontend
auto-aufgerufen nach erfolgreichem activateUrlFilter
Bypass-Recovery durch externe iOS-Settings-Disable (nicht cooldown-bezogen)
funktioniert weiter — protectionDisabledAt ist dann null, alte Logik greift.
## Frontend: ProtectionOffSheet (Custom-Sheet statt Alert.alert)
Bisheriges native Alert mit OK+Reactivate-Buttons hat keine visuelle
Hierarchy (iOS macht beide gleich). Ersetzt mit FormSheet:
- Großer blauer Primary "Schutz wieder einschalten"
- Ghost-Link "Später"
- Swipe-down / Backdrop-Tap zum Schließen
## Frontend: ProtectionSlide mit Pre-Explainer (Screenshot + Pulse-Marker)
User-Request: vor dem iOS-Permission-Dialog ein Erklärungs-Screen zeigen
damit der User weiß wo er tappen muss (Apple's "Don't Allow" ist groß+
blau = Trap, "Allow" ist der unscheinbare Button unten).
- components/onboarding/ScreenshotPointer.tsx — Reanimated pulsing red
ring, positionierbar via {xPercent, yPercent}
- lib/onboardingAssets.ts — locale-aware require()-Map für Screenshot-
Assets mit de-Fallback
- assets/onboarding/de/ — 4 iOS-Screenshots vom User (url_filter +
screen_time permission dialogs + 2 confirm screens)
- ProtectionSlide refactored: internal phase state preexplain_url →
preexplain_lock → done. Jede Phase zeigt Screenshot + Pulse-Marker auf
korrekten Button + Lyra-Bubble + activate-CTA.
## Locale-Keys
- onboarding.lyra.protection_url.body, onboarding.lyra.protection_lock.body
- onboarding.protection.url_title, .lock_title, .tap_marker_hint
- onboarding.protection.applock_failed_*, applock_skip
- blocker.protection_off_later, reactivate_btn (refined)
## Bugfix: de.json JSON-syntax
Smart-quote-typo: schließendes "" nach „Erlauben" und „Fortfahren" war
ein plain ASCII " (U+0022) statt U+201D, was den JSON-String früh
terminiert hat. Metro+Hermes warfen "unrecognized Unicode —".
Fix: escapte \" verwendet — JSON-safe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
State of work before Duo-style onboarding pivot. Includes work that will be
partly reverted in the next commit (see refactor follow-up).
Onboarding (will be partly reverted):
- Custom Tooltip+Glow spotlight (components/OnboardingHint.tsx)
- Spotlight wiring in app/profile/edit.tsx (nickname-input glow + step-progress
header, onSubmitEditing auto-save, save-handler routes to /(app)/blocker)
- Spotlight wiring in app/(app)/blocker.tsx (URL-filter LayerSwitchCard wrapped
+ auto-PATCH step='done' when filter activates)
- Routing-gate branches in (app)/_layout.tsx (welcome → /onboarding/welcome,
nickname → /profile/edit)
- Debug-Reset-Toggle in /debug (welcome|nickname|block|done buttons + redirect)
Will stay (reused in Duo flow):
- Welcome-Screen app/onboarding/welcome.tsx (will become Slide 1)
- Avatar-fix in profile/edit (Dicebear seed stays stable while typing)
i18n + RTL:
- Arabic locale (locales/ar.json, full translation incl. onboarding keys)
- I18nManager.allowRTL(true) + applyRTL helper in stores/language.ts
- Language-Picker option for العربية in settings
- New keys: onboarding.welcome.*, step_progress, nickname_spotlight.*,
block_spotlight.*, permission_denied.*, language.*, rtl_restart.* (de/en/fr/ar)
NEFilter Permission Recovery (iOS):
- Swift resetUrlFilter() — removeFromPreferences + fresh saveToPreferences to
bypass iOS's cached denied-state (NEFilterErrorDomain code 5)
- TS module def + lib/protection.ts wrapper
- components/PermissionDeniedSheet.tsx — branded recovery sheet with retry +
app-settings:// deep-link + fallback hint
- Wired in (app)/blocker.tsx handleActivateUrlFilter (code-5 detection)
Misc:
- Bug fix in onboarding/welcome.tsx: apiFetch body was double-stringified (sent
as JSON string instead of object → 400 invalid_step)
- Bug fix in profile/edit.tsx: avatar preview Dicebear seed switched from live
nickname (changed every keystroke) to stable me?.nickname
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- with-rebreak-protection-android plugin now copies the source
accessibility_service_config.xml via withDangerousMod instead of generating
it from a string. Eliminates the silent regression where prebuild wrote
flagReportViewIds + missing packageNames, leaving Samsung's content scan
unable to read OEM dialogs.
- ProtectionOnboardingSheet refresh() now calls activateFamilyControls()
once a11y is detected as enabled, so armTamperLock() actually runs.
Previously the sheet auto-completed on getDeviceState() alone, leaving
tamper_armed=false and the service permanently passive.
- RebreakProtectionModule.isAccessibilityServiceEnabled() now trusts the
AccessibilityManager list as authoritative when AM is available (even when
empty). Settings.Secure fallback only kicks in if AM is null/exception.
Fixes the banner falsely showing "Schutz aktiv" when the system has
unbound the service but ENABLED_ACCESSIBILITY_SERVICES still holds the id.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Blocker banner: show real accessibility status on Android (active/inactive)
instead of the iOS Family-Controls "bald verfügbar" fallback
- AppState listener refreshes state when user returns from system settings
- New ProtectionOnboardingSheet: enforced order VPN → a11y because once a11y
is on it locks VPN settings access. Step 2 disabled until step 1 done.
Skip is allowed; storage flag set only after both steps complete.
- i18n: blocker.layers_a11y_subtitle_active/inactive + protection_onboarding.*
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Header: partner avatar left-aligned (was centered)
- ChatBubble: replace bright blue with subtle mint/brand tint, asymmetric
tail-corner radius, footer pinned bottom-right, reply-quote with green
side-bar
- New DmChatBackground: SVG hex-offset doodle pattern (stars, hearts,
clouds, dots) at 7% opacity — light-cream / dark-warm-green base
- Avatar in chat list: use resolveAvatar() consistently to handle
hero-id, https, and null cases
- Realtime subscription: stabilize deps via partnerRef to stop
re-subscribing on partner state change
- Pressable → TouchableOpacity throughout
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User saw entries like "vor 61d · Outlook" under the "Kürzlich
blockiert · In den letzten 24h" header. createdAt (when the daemon
wrote the mail_blocked row) is always inside the 24h retention window
because deleteOldMailBlocked sweeps everything older than that on
every fetch — but the row preserves the original receivedAt header
from the email, which for old Casino mails the daemon only just got
around to scanning can be weeks or months ago.
Switched the time-label in MailActivityLog to format createdAt
instead. The MailBlockedItem type now carries createdAt explicitly
(the backend has been returning it all along, the FE type just hadn't
acknowledged it). receivedAt stays in the shape for any future
"received vs blocked" comparison view but isn't used in the recent-
activity list anymore.
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.