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>
expo-local-authentication already works on Android (fingerprint / face unlock /
device PIN — the module auto-adds the biometric permission, no app.config change
needed). Only the settings description was iOS-flavoured ("Face ID, Touch ID");
add an Android variant and pick by Platform.OS. The lock screen + biometric
prompt strings were already generic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
appLock.ts is imported from the root layout, so an unguarded top-level
`import * as LocalAuthentication from 'expo-local-authentication'` crashes the
whole app at launch on a dev-client built before the dependency was added
("Cannot find native module 'ExpoLocalAuthentication'"). Load it via a guarded
require; if absent → app lock reports `available: false`, everything else runs.
Real builds (EAS / fresh prebuild) ship the module and work normally.
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>
The TestFlight build kept failing with:
error: No profiles for 'org.rebreak.app.RebreakURLFilter' were found ...
Automatic signing is disabled and unable to generate a profile.
(in target 'RebreakURLFilter' from project 'ReBreak')
EAS managed credentials only provision the main app bundle ID. App extensions
must be declared up-front via extra.eas.build.experimental.ios.appExtensions so
the CLI knows to register the extension's App ID (+ app-group + network-extension
capabilities) and generate a distribution provisioning profile for it.
Next step (interactive, needs Apple login): `eas credentials` → iOS → preview
to actually create the extension credentials, then re-run the build.
(FamilyControls entitlement stays commented out in with-rebreak-protection-ios.js
until Apple grants the Distribution entitlement — this build ships without App-Lock.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The EAS error message "Xcode 14 resource bundle signing" was a misleading wrapper.
Pulled the actual Xcode log via the EAS CLI; the real failures were:
error: Provisioning profile "...AppStore..." doesn't support the Family Controls
(Development) capability.
error: Provisioning profile ... doesn't include the com.apple.developer.family-controls
entitlement.
error: Signing for "RebreakURLFilter" requires a development team. (in target
'RebreakURLFilter' from project 'ReBreak')
Two fixes:
1. Family Controls is requested with Apple but not yet granted (Distribution), so
EAS can't generate an AppStore provisioning profile that includes it → comment
out the family-controls entitlement claim in withMainAppEntitlements. Re-enable
once Apple grants the entitlement. The iOS Swift code still imports
FamilyControls/ManagedSettings (public frameworks, link fine without the
entitlement); activateFamilyControls would throw at runtime — handled by the
JS layer's catch. Net: TestFlight build works, iOS App-Lock feature is dormant
until the entitlement lands.
2. The RebreakURLFilter extension target had no DEVELOPMENT_TEAM set — EAS managed
credentials only set it on the main app target; sub-targets don't inherit.
Hardcoded the team ID 84BQ7MTFYK on the extension's build configurations
(matches eas.json submit.production.ios.appleTeamId).
(The resource-bundle-signing fix from the previous attempt stays — it's
not the cause here but is correct hygiene for static-frameworks builds.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First attempt targeted only `target_installation_results.resource_bundle_targets` —
too narrow. With privacyManifestAggregationEnabled the Pods project has additional
bundle targets (aggregated privacy manifests) that also need code-signing disabled.
Brute-force fix: set CODE_SIGNING_ALLOWED/REQUIRED = NO and clear
EXPANDED_CODE_SIGN_IDENTITY on every target in installer.pods_project — pod
targets don't need signing, only the main app does. Added a Pod::UI.puts so we
can see the fix run in the EAS build log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EAS iOS build failed: "Starting from Xcode 14, resource bundles are signed by
default, which requires setting the development team for each resource bundle
target." Cause: we build with useFrameworks: "static" (expo-build-properties),
so CocoaPods generates resource-bundle targets for pods with resources, and
Xcode 14+ wants them signed. EAS has no dev team for those.
New plugin with-resource-bundle-signing-fix injects into the Podfile's existing
post_install hook: sets CODE_SIGNING_ALLOWED = 'NO' for every pod resource-bundle
target (they don't need signing). Idempotent; runs as withDangerousMod('ios')
during prebuild so it survives EAS's clean prebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The preview/production profiles referenced channel "preview"/"production" which
requires expo-updates + `eas update:configure`. Not needed for the TestFlight
build; dropping them avoids the install prompt. (Re-add when we deliberately
wire up EAS Update / OTA.)
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>
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>