- 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>
After an APK reinstall (or an OS low-memory kill that START_STICKY didn't recover
promptly), the VpnService dies but `filter_enabled` stays true. isVpnEffectivelyOn
then reports vpn:true (from the flag) → tamperLock:true → lockedIn:true → the green
"protection active" card with no toggles, while in reality nothing is filtering.
New native reconcileVpn(): if `filter_enabled` && !RebreakVpnService.isRunning &&
VpnService.prepare()==null → startVpnService(). Wired into _layout.tsx enforceProtection()
(runs on launch / foreground / 15s poll), called before reading combined state. No-op
on iOS/web. If the VPN consent was revoked, isVpnEffectivelyOn already clears the flag,
so that case self-resolves too.
Net behavior: while `filter_enabled` is true (user hasn't exited via the cooldown),
the app keeps the VPN alive. Exiting still goes through the cooldown → forceDisable →
filter_enabled=false → reconcile leaves it off. DiGA-compliant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The a11y (App-Lock) permission flow now runs only the first time the user turns
protection on. Reactivating after a cooldown / external disable just re-starts the
VPN/DNS filter — no a11y system prompt, no modal loop ("a11y can't be activated…").
- blocker.tsx handleActivateFamilyControls: no error modal when error === 'accessibility_pending'
(we just opened the a11y settings — that's the feedback; tapping again re-opens, no loop).
- lib/protection.ts getCombinedState: "active" = urlFilter on (App-Lock is optional hardening,
not a precondition); "recoveringFromBypass" now means urlFilter is OFF while the backend
says it should be on (a real bypass), instead of "lock is off".
- blocker.tsx recoveringFromBypass alert: offers "turn back on" → activateUrlFilter (VPN),
not activateFamilyControls.
- _layout.tsx bypass re-arm (enforceProtection fallback + onBypassNotificationTap):
protection.activate() instead of activateFamilyControls().
- new i18n keys: blocker.protection_off_title / protection_off_message / reactivate_btn.
JS-only (hot-reloadable).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Repro: after a reinstall / external VPN-revoke, `filter_enabled` flipped to false
but `tamper_armed` stayed true. Result: buildDeviceState reported tamperLock:true
purely from `tamper_armed` → UI mapped that to appDeletionLock:true → lockedIn:true
→ showed the green "protected & locked" card with no toggles → no way to reactivate.
(The a11y service didn't block — handleProtectedSettingsBlock checks isProtectionEnabled
— but it kept logging every settings-navigation, wasting CPU.) "Armed but disabled"
is an invalid state.
- RebreakAccessibilityService: top guard is now `if (!isTamperLockArmed() || !isProtectionEnabled()) return`
— fully passive (no logging) whenever protection is off, regardless of a stale tamper flag.
- RebreakProtectionModule.buildDeviceState: tamperLock = tamper_armed && filter_enabled.
- RebreakProtectionModule.isVpnEffectivelyOn (revoke branch) and RebreakVpnService.onRevoke
now clear `tamper_armed` together with `filter_enabled` — the two can't desync.
Self-heals: opening the blocker page after the update re-fetches state → tamperLock:false → toggles back.
Also: the tamper-block toast is now Lyra-voiced instead of a shield emoji (a real avatar
image isn't possible — Android 11+ ignores Toast.setView() for app toasts; lyra-persona
can refine the wording).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The AccessibilityService used to also do a browser-address-bar filter (read the
URL bar of Chrome/Firefox/etc., hash-match against blocklist.bin, GLOBAL_ACTION_BACK
on a hit) as a "layer 2" alongside the VpnService DNS filter. That's redundant
(the VPN catches everything network-level, in browsers AND apps), fragile (per-browser
view-IDs), and produced ghost-blocks (VPN off, a11y still blocking sites). The DNS
filter is the protection; the a11y service's only real value-add is tamper-resistance.
So the a11y service now does ONLY the tamper-lock, and only when the user has armed
"App-Lock": block opening protection-critical settings (disable the ReBreak VPN,
uninstall the app, disable the a11y service itself). Top-level guard is now simply
`if (!isTamperLockArmed()) return` — when App-Lock isn't armed the service is fully
passive. Getting out is still via the regular deactivation cooldown (which disarms
the tamper-lock and stops the VPN).
- RebreakAccessibilityService.kt: removed browser-URL extraction, BROWSER_PACKAGES,
URL_BAR_IDS, hashList loading, throttle bookkeeping, the block-toast. Kept the
settings-watchdog (it already covered VPN settings via VpnSettings/vpndialogs +
the vpn-page keyword cluster) and adjusted its keyword lists to the new a11y
service summary (old summary kept as a legacy fallback for stale installs).
- accessibility_service_config.xml: dropped browser packages + flagRequestEnhancedWebAccessibility.
- strings.xml (de+en): a11y permission copy reframed — it safeguards the VPN/uninstall,
it doesn't filter your browser; ends with "you can always exit via the cooldown".
- lib/protection.ts: comment-only (activateFamilyControls logic unchanged).
- locales de/en: App-Lock card copy ("Familienzugriff aktiv" → "Verriegelt — ...",
"...ReBreak oder den Filter im Impuls abschaltest"), genericised the iOS Screen-Time
error string.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apps/rebreak-native/.gitignore had bare `ios/` + `android/` patterns meant for the
Expo-prebuild output dirs — but with no leading slash they also matched
modules/rebreak-protection/{android,ios}, so the entire custom expo native module
(RebreakProtectionModule.kt, RebreakAccessibilityService.kt, RebreakVpnService.kt,
the DNS filter, the iOS NEFilter extension, podspec, ...) was never tracked. A
fresh clone / CI / `git clean` would lose it.
Anchor the prebuild patterns (`/ios/`, `/android/`), keep ignoring the module's
build artifacts (build/, .cxx/, .gradle/, Pods/), and commit the source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs caused the domainRealtime channel to fail with CHANNEL_ERROR and
reconnect-loop every 3s (which also dragged down the notifRealtime channel via
the shared websocket close):
1. useDomainSubmissionRealtime.ts filtered domain_submissions on a column that
doesn't exist (`submitter_id`) — the actual column is `user_id`. Postgres
raised on the publication-side filter registration → CHANNEL_ERROR.
2. rebreak.user_custom_domains was never added to the supabase_realtime
publication — the channel also subscribes to that table. New migration
20260511_fix_realtime_user_custom_domains adds it.
(Diagnosis via backyard agent against the self-hosted Supabase on the Hetzner box.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the cooldown elapses and forceDisable() runs (VPN off + tamper-lock
disarmed), Android's a11y service can't deactivate itself — surface a friendly
Alert routing the user to Settings → Accessibility so they can finish removing
protection. Wired into both the fetchState cooldown active→inactive transition
and the AppState 'active' check; idempotent via ref.
(Native side — disable() also disarms the tamper-lock, RebreakAccessibilityService
goes fully passive when neither tamper-locked nor enabled, syncBlocklist no longer
re-starts the VpnService when disabled — lives in the gitignored module/android dir,
not committed here.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the cooldown elapsed and forceDisable() stopped the VPN, the tamper_armed
SharedPref flag was left set → the AccessibilityService kept enforcing protection
(e.g. blocked the user from turning the a11y service off in system Settings) →
the user couldn't actually get out of protection despite the cooldown elapsing.
forceDisable() now calls disarmTamperLock() before disable().
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>
debug.tsx: removed admin-403 special-case, calls /api/dev/set-plan directly.
settings.tsx: new PlanPickerSheetContent (TrueSheet, DEV-only) in debug section
with three plan options; uses same endpoint + invalidateMe().
Co-Authored-By: Claude Sonnet 4.6 <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>
free: 5 custom domains (non-refillable), no global blocklist, 1 mail account,
basic coach. pro: global blocklist (the headline), 5 refillable domains, 3 mail
accounts (cron choice, no daemon), better coach. legend: + IMAP-IDLE daemon
(real-time mail scan — app highlight), 10 refillable domains with ≤24h ReBreak
validation, much better coach, +2 device DNS profiles. Marketing critique (§7)
pending from strategist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/dev/set-plan { plan: 'free'|'pro'|'legend' } — requireUser, sets the
caller's own profile.plan via Prisma. Refuses on production URL (same guard as
the cooldown testMode: appUrl includes rebreak.org && !includes staging). Lets
the __DEV__ tier-toggle work without admin rights. Does NOT weaken updateProfile.
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>
Infisical staging holds the bot user IDs as NUXT_LYRA_BOT_USER_ID /
NUXT_REBREAK_BOT_USER_ID, but nitro.config.ts reads process.env.LYRA_BOT_USER_ID
(no NUXT_ fallback) and start-staging.sh had no alias for them → config.lyraBotUserId
was empty → POST /api/admin/lyra-post threw 500 "LYRA_BOT_USER_ID nicht
konfiguriert" (surfaced via the admin app proxy). Adds the alias + NITRO_ override
exports, same pattern as the other keys.
Also: ops/strategy/pricing-tiers.md — strategist's tier-pricing analysis,
stress-test, downgrade-policy matrix, plan-change briefing-screen content +
scenario test matrix (Task #8 Phase 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useUserPlan had its own module-level cache + fetch-once-on-mount, separate
from useMe's invalidateMe(). So the __DEV__ tier-override toggle (which calls
invalidateMe()) never reached useUserPlan consumers → the app didn't react to
a plan switch. Now useUserPlan just reads me.plan from useMe → inherits its
live-invalidation, the toggle propagates everywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pages/lyra.vue page (create community posts as Lyra/ReBreak, AI-generated
or manual) existed but wasn't linked anywhere. Adds it to the sidebar +
mobile bottom-tab (grid-cols-5→6) and the dashboard quick-links grid
(lg:grid-cols-4→5). Admin app stays team-internal (stats / users / domain
approval / social posts / moderation) — no relation to the RN app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Old adaptiveIcon was a full-bleed dark logo on a #0a0a0a background → the
launcher mask cropped it ("zoomed in" look). Now: white background (matches
the Play Store listing icon look) + the same logo at ~62% on a transparent
canvas → mask has nothing to clip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes the metro-symbolicate version skew (nested 0.82.5 copies under
metro@/metro-source-map@) that broke the release JS bundle step
("SourceMetadataMapConsumer is not a constructor" in composeSourceMaps).
Required a clean reinstall (rm -rf node_modules && pnpm install) to drop
the stale nested dirs.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
- settings.tsx: real "Abo" row showing current plan (Free/Pro/Legend badge),
taps open a sheet explaining subscriptions are managed on rebreak.org
(Linking.openURL → /account; TODO: gate for iOS App-Store submission per
Apple 3.1.1 — no in-app purchase flow)
- debug.tsx: __DEV__-only plan-override toggle (free/pro/legend) via
PATCH /api/admin/users/:id + invalidateMe(); shows admin-only hint on 403
- locales: settings.subscription_* keys (de + en)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes [notifRealtime] CHANNEL_ERROR — table was not in supabase_realtime
publication, so postgres_changes events never arrived. Added by backyard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Memory feedback_known_ui_layout_bugs.md Pattern 5: Pressable with
style={({pressed}) => ({...})} is layout-poison in some RN-render-paths,
button collapses to 0-height and renders invisible. Windows-button right
below worked because it uses static style={{...}}.
TouchableOpacity gets same press-feedback via activeOpacity prop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugin referenced @string/accessibility_service_summary +
@xml/accessibility_service_config in AndroidManifest but never created the
underlying resource files. EAS Cloud prebuild --clean exposed this — local
dev worked because resources were sometimes already there from previous builds.
- withStringsXml: adds accessibility_service_summary string (DE)
- withDangerousMod: writes res/xml/accessibility_service_config.xml at prebuild
- Config flags match native service (TYPE_WINDOW_CONTENT_CHANGED + STATE_CHANGED,
canRetrieveWindowContent for URL-bar reading)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EAS Cloud prebuild ignores local android/build.gradle pins (android/ is gitignored).
Plugin compileSdk 35 → 36 satisfies new androidx.core dependency requirements.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
- ProtectedDevice prisma model + migration add_protected_devices
- DB helpers: list/count/get/create/confirm/revoke
- mobileconfig.ts utility — XML-escape, unique UUIDs per request
- 5 endpoints under /api/devices/* (avoid /api/devices conflict with existing
Capacitor UserDevice route by using /api/devices/protected for list)
Phase 1: backend ready. DoH-server token-routing comes in phase 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
metro-cache 0.82.5 (nested under metro 0.82.5) imports metro-core/src/canonicalize
directly. Top-level metro-core 0.83.3 has restrictive exports map that blocks this.
Pnpm patch adds ./src/* to exports while preserving the existing ./private/* path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>