608 Commits

Author SHA1 Message Date
chahinebrini
c6604f02df chore(release): v0.3.4 — enable Family Controls (Apple distribution entitlement approved) 2026-05-20 07:13:13 +02:00
chahinebrini
ea152a9169 feat(debug): debug-page in TestFlight via EXPO_PUBLIC_ENABLE_DEBUG flag
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.
2026-05-20 05:42:00 +02:00
chahinebrini
5a44b8162e chore(release): v0.3.3 — FC-gating + native log-viewer + expo-image 2026-05-20 05:34:57 +02:00
chahinebrini
f318364c7e fix(protection): gate Family Controls on entitlement + native log viewer
Verifiziert via Build-11-.ipa-Inspektion + Device-Logs:
- com.apple.developer.family-controls FEHLT im signierten Distribution-Build
  (Plugin gated korrekt auf REBREAK_ENABLE_FAMILY_CONTROLS — Apple-
  Distribution-Approval pending).
- Device-Log beweist: ManagedSettingsAgent-XPC → "error 159 Sandbox
  restriction" → NSCocoaErrorDomain:4099. Kein Daemon-/Race-Problem,
  Retry zwecklos.

Root-Cause des FC-Bugs: app.config.ts hatte familyControlsEnabled hart
auf true (falscher "approved"-Kommentar) → App bot App-Lock an obwohl
sandbox-blockiert. FAMILY_CONTROLS_AVAILABLE wurde nirgends konsumiert.

Fixes:
- app.config.ts: familyControlsEnabled an REBREAK_ENABLE_FAMILY_CONTROLS
  gekoppelt (== Plugin-Gating) → in TestFlight/production false.
- blocker.tsx: iOS App-Lock-Card nur wenn FAMILY_CONTROLS_AVAILABLE.
  lockedIn akzeptiert URL-Filter allein wenn FC build-seitig fehlt.
- ProtectionSlide.tsx: Onboarding überspringt den App-Lock-Step (FC)
  wenn nicht verfügbar — URL-Filter allein = vollwertiger Schutz.

Native Log-Viewer (für NEFilter-Debug ohne Mac/Console.app):
- Swift: getProtectionLogs/clearProtectionLogs lesen SharedLogStore aus
  App-Group-UserDefaults.
- lib/protection.ts wrapper + TS-module-types.
- debug.tsx: ProtectionLogCard (iOS) — native NEFilter/FC-Logs in der
  App sichtbar, copy/clear/refresh.
2026-05-20 05:32:06 +02:00
chahinebrini
b8e4b02b88 perf(images): migrate react-native Image → expo-image (memory+disk cache)
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).
2026-05-20 04:49:11 +02:00
chahinebrini
a9015d1951 chore(release): v0.3.2 — bug-fix bundle (permission/cooldown/onboarding) 2026-05-20 04:30:24 +02:00
chahinebrini
306a8fda30 fix(blocker): cooldown-elapsed modal platform-specific (no Android text on iOS)
Modal zeigte auf iOS "Du kannst den ReBreak-Bedienungshilfe-Dienst jetzt
in den Einstellungen ausschalten" — Bedienungshilfe/Accessibility-Service
ist ein Android-Konzept, existiert auf iOS nicht.

iOS: NEFilter + Family Controls werden von forceDisable() vollständig
abgeschaltet, User muss nichts in Settings tun. Neue iOS-Variante zeigt
nur "Cooldown abgelaufen — Schutz deaktiviert." + OK, kein Settings-Button.

Android: unverändert (a11y-Service braucht Settings-Deeplink).

i18n DE/EN/FR/AR: cooldown_elapsed_message_ios neu.
2026-05-20 04:25:15 +02:00
chahinebrini
312c668ae9 feat(onboarding): back-button between steps + language switcher on welcome
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.
2026-05-20 04:20:22 +02:00
chahinebrini
34005803da fix(protection): cooldown-elapse must set protectionDisabledAt in state endpoint
Bug: nach Cooldown-Ablauf reaktivierte sich der Schutz automatisch.

Root-Cause: Race zwischen zwei Endpoints die abgelaufene Cooldowns auflösen.
/api/cooldown/status setzt korrekt profile.protectionDisabledAt. /api/
protection/state — alle 5s während Cooldown gepollt — machte beim Ablauf
NUR resolveCooldown(), ohne protectionDisabledAt. state.get gewann das Race
fast immer → protectionDisabledAt blieb null → protectionShouldBeActive=true
→ Frontend-Bypass-Detection reaktivierte den Schutz.

Fix: state.get.ts setzt im expired-Branch ebenfalls protectionDisabledAt +
cooldownJustResolved-Flag.
2026-05-20 04:17:24 +02:00
chahinebrini
c32eeeb070 fix(protection): NEFilter retry + FamilyControls 4099 recovery sheet
Tester reports: nach 'Don't Allow' im System-Dialog reagiert Re-Request
nicht (NEFilter), plus FamilyControls wirft NSCocoaErrorDomain:4099
(XPC-Daemon-Failure). Mehrere TestFlight-User betroffen.

Swift native:
- resetUrlFilter: 800ms delay nach remove + 3x retry-loop bei code 5
- activateFamilyControls: 3x retry-loop mit Backoff bei 4099

JS:
- PermissionDeniedSheet generic via variant prop (nefilter|family_controls)
- Blocker + Onboarding: 4099-detect → Recovery-Sheet mit 3-Step-Fallback

I18n DE/EN/FR/AR: blocker.family_controls_error.* keys
2026-05-20 03:51:33 +02:00
chahinebrini
73f70b5e28 fix(language): auto-sync from user_metadata.locale at sign-in
Bug: User mit iOS-Sprache=Arabisch sah App auf Englisch wenn
Localization.getLocales() auf seinem Setup nicht zuverlässig 'ar'
zurückgab (iOS-Region≠Sprache, App-Override etc).

Fix: bei sign-in (init() initial-getSession + onAuthStateChange für
SIGNED_IN events) wird session.user.user_metadata.locale gelesen.
Wenn AsyncStorage @rebreak/language NOCH NICHT gesetzt ist (User hat
keine explicit Choice gemacht) → silent apply der server-locale
(inkl. RTL-flip, KEIN Restart-Alert).

Respektiert User-Choice: wenn AsyncStorage gefüllt ist (z.B. User hat
manuell in Settings gewechselt), bleibt das gewinnen.
2026-05-19 22:02:34 +02:00
chahinebrini
63a1a3b550 fix(staging): export NITRO_* env-mapping for BREVO_API_KEY + hook secrets
start-staging.sh re-exports Infisical secrets als NITRO_*-prefixed envs
damit Nitro's runtimeConfig sie zur Laufzeit overrided (Default ist
Build-Time evaluated, also "" wenn nicht im Runner-Env).

Drei neue Mappings:
- BREVO_API_KEY → NITRO_BREVO_API_KEY
- HOOK_SEND_EMAIL_SECRETS → NITRO_HOOK_SEND_EMAIL_SECRETS
- MAIL_SENDER_EMAIL → NITRO_MAIL_SENDER_EMAIL

Ohne diese Mapping bleibt config.brevoApiKey leer im laufenden Backend →
send-email Hook fired 500 (siehe Smoke-Test).
2026-05-19 18:32:01 +02:00
chahinebrini
bdfcc40a6c feat(auth-hook): send-email hook for dynamic sender-name + i18n subject
GoTrue-Send-Email-Hook (v2.155+) → backend rendert Mails selbst, schickt
via Brevo Transactional API. Vorteil: pro Mail-Typ × Locale eigener
Sender-Display-Name UND Subject (GoTrue's eingebauter Mailer ist global
statisch).

Files:
- backend/server/utils/mail/templates.ts — 5 mail-types × 4 locales
  Sender-Name, Subject, Title, Body, HelpText, Privacy-Label.
  HTML-Renderer mit Nunito + Icon + DSGVO-Footer (analog public/templates).
- backend/server/utils/mail/brevo.ts — Brevo Transactional API client
  (POST /v3/smtp/email, separate REST-API-Key vom SMTP-Key)
- backend/server/api/auth-hooks/send-email.post.ts — Endpoint mit
  Standard-Webhooks Signature-Verify (HMAC-SHA256, timing-safe compare,
  multi-secret-rotation support)
- backend/nitro.config.ts — runtimeConfig: brevoApiKey,
  hookSendEmailSecrets, mailSenderEmail

Requires (außerhalb dieses commits):
- Infisical staging: BREVO_API_KEY (✓), HOOK_SEND_EMAIL_SECRETS (✓)
- docker-compose: GOTRUE_HOOK_SEND_EMAIL_{ENABLED,URI,SECRETS} (✓)
- GoTrue upgrade v2.154 → v2.189 (✓, mit auth.factor_type owner-transfer)
2026-05-19 18:04:14 +02:00
chahinebrini
a0dff80ced ui(signup): privacy-notice shield als app-icon-look
🛡️ Unicode-Schild → grünes (22c55e) rounded-md Square mit weißem
Ionicons shield-checkmark, 32x32. Konsistent mit App-Icon-Style.
2026-05-19 17:57:02 +02:00
chahinebrini
a8e094be9d fix(mail-templates): banner layout — icon left, wordmark centered
Vorher: Icon und 'ReBreak'-Wordmark untereinander (vertikal gestapelt).
Jetzt: 3-Spalten-Tabelle — Icon links (48x48), Wordmark mittig,
gleichgroßer Spacer rechts (52px) für visuelle Mitte.

Icon-Größe von 64x64 → 48x48 reduziert für besseres Höhen-Verhältnis.
2026-05-19 13:34:25 +02:00
chahinebrini
f2545738ee feat(mail-templates): Nunito font, app-icon header, DSGVO footer
- App-Icon (128x128, resized von 1024x1024, 6.7KB) im dunklen Header
  über dem 'ReBreak'-Wordmark, abgerundete Ecken (border-radius:14px)
- Nunito (Google Fonts, weights 400/600/700) für allen Body-Text,
  Inline font-family auf jedem Element wegen Gmail-Style-Stripping.
  OTP-Code bleibt monospace (Apple SF Mono / Menlo)
- DSGVO-Footer: rebreak.org + Datenschutz-Link (i18n'd: Datenschutz /
  Privacy / Confidentialité / الخصوصية)
- 5 Templates konsistent: confirmation, recovery, magic_link, invite,
  email_change
2026-05-19 11:10:49 +02:00
chahinebrini
12e140e25b refactor(signup): remove firstName/lastName fields (DSGVO data-minimization)
Felder wurden nirgendwo gelesen/angezeigt (nur in raw_user_meta_data
gespeichert ohne Verwendung). Inkonsistent mit OAuth-Flow der sie
gar nicht erfasst. Entfernt:
- 2 Inputs aus signup.tsx
- firstName/lastName aus signUp metadata-Typ + data
- 8 i18n-keys (de/en/fr/ar)
- DB-Cleanup via SQL für 5 existing User (raw_user_meta_data - 'first_name' - 'last_name')

Art. 5(1)c DSGVO: nur Daten verarbeiten die für Zweck notwendig sind.
Nickname allein reicht — Anonymität-Pattern (memory/feedback_anonymity_nickname.md).
2026-05-19 11:05:18 +02:00
chahinebrini
512949f851 fix(nitro): explicit publicAssets dir=../public so templates ship in .output
Nitro resolved default publicAssets `dir: "public"` relativ zu srcDir
(`server/`) statt zum rootDir. Folge: backend/public/templates/*.html
wurde nicht in .output/public/ kopiert → GoTrue lädt 404.

Fix: explicit `dir: "../public"` zeigt auf <projectRoot>/public/.
2026-05-19 10:58:18 +02:00
chahinebrini
5434254f74 feat(auth,mail): pw-reset OTP-flow + custom mail templates + account-switch cleanup
- Phase 3 PW-Reset: 3 screens (forgot-password → reset-otp → new-password),
  verifyOtp({type:'recovery'}), new updatePassword() action
- Custom Brevo-Mail templates (backend/public/templates/) — 5 HTMLs with
  go-template i18n (de/en/fr/ar incl. RTL for AR), OTP-only (no link),
  ReBreak branding
- signUp metadata.data.locale aus i18n.language → templates resolven Sprache
- Account-Switch-Bug fix: signOut() resettet alle 10 user-spezifischen stores
  + invalidateMe()
2026-05-19 10:49:23 +02:00
chahinebrini
b9b397b346 chore(release): v0.3.1 + eas-release.sh script
- package.json + app.config.ts (Expo source-of-truth) auf 0.3.1
- versionCode/buildNumber bleibt 10 (native files schon synced)
- eas-release.sh: build+submit für TestFlight/Play-Internal, default
  iOS, --android/--both flags, version-check-prompt vor build

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 08:11:18 +02:00
chahinebrini
5c539f8937 feat(presence,sheets,chat): tester-build polish bundle
Online-Status (Phase 1+):
- UserAvatar mit 4 Size-Variants (sm/md/lg/xl) + integrierter Online-Dot
- OnlinePresenceProvider: Supabase-Channel + Following-Filter
- ChatHeaderStatus: "Online" neutral / "vor X min" offline
- useLastSeen + Heartbeat (60s interval + AppState-background ping)
- Privatsphäre-Toggle in profile/index

Sheets:
- FormSheet Android-keyboard-fix (Dimensions.get('screen'), kein
  useWindowDimensions-Kollaps), useKeyboardHandler statt manual
  Keyboard.addListener, state-reset on re-open
- PostCommentsSheet same Pattern + close-after-submit + drag bis under
  app-header
- ConnectMailSheet form-view refactor: scrollable, AES-Banner als
  footnote, field-order email→pw→label, fixed 0.85 über alle Steps

Chat:
- DmChatBackground iOS klecks fix (G transform statt nested Svg)
- ChatInput Lyra-1:1 (keyboardWillShow, surfaceElevated bubble,
  arrow-up send, attachment links)
- dm/room/chat headers + conversation-list nutzen UserAvatar
- Foreign-Profile "Nachricht"-Button öffnet richtige DM

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 08:06:47 +02:00
chahinebrini
19b569927a fix(presence): missing imports in 3 endpoints
Same pattern as touchLastSeen: getLastSeenBatch, setPresenceVisible,
getFollowingIds wurden im db/profile.ts implementiert aber nicht in den
Endpoints importiert. Alle 3 warfen 500 ReferenceError → grüner Dot
zeigte sich nie + Toggle silently failed.

Nitro's auto-import covered nur defineEventHandler/getQuery etc., NICHT
unsere eigenen db-layer helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 07:16:18 +02:00
chahinebrini
c89f541069 fix(presence): missing touchLastSeen import in /api/me/last-seen
Endpoint warf 500 jedem Heartbeat-Call (ReferenceError) — Floodete
pm2-Logs. Import war im DB-Layer-File implementiert aber nicht vom
Endpoint importiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 06:35:11 +02:00
chahinebrini
0ca0afb1e1 feat(presence): online-status backend (Phase 1)
Insta-style Online-Status mit Following-Filter + User-opt-out:
- Profile.lastSeenAt + Profile.presenceVisible (default true)
- GET /api/presence/last-seen?userIds=... batch, server-side filter
  durch Follow-Relation + presenceVisible
- GET /api/me/following → User-IDs für client-side Channel-Filter
  (Supabase Realtime Presence hat keine server-side Filter)
- POST /api/me/presence-visibility Toggle
- POST /api/me/last-seen Heartbeat (Phase-1-Fallback bis Edge-Function)
- /api/auth/me extended um presenceVisible für Settings-Initial-State

DB-Layer nutzt raw SQL bis Migration auf staging gelaufen ist
(Prisma-Client refresh erst nach CI generate).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 06:23:08 +02:00
chahinebrini
3c73a8b44a feat(community/posts): ?userId= query filter
Foreign-Profile-Page rendert User-Posts jetzt via PostCard mit dem
gleichen Daten-Shape wie Index. `?userId=<uuid>` filtert auf Posts
dieses Users — kombinierbar mit `category=...`, Bot-Kategorie-Mapping
wird übersteuert wenn explizite userId mitgegeben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 04:06:13 +02:00
chahinebrini
bca00e03a5 feat(social/profile): approvedDomainsCount in foreign-profile response
Frontend Foreign-Profile-Page rendert 3 Stat-Cards (Posts/Followers/
Approved) — counter für approved DomainSubmissions hat im Endpoint
gefehlt → undefined im UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 03:45:17 +02:00
chahinebrini
c6e2116084 feat(community): admin/lyra-post Multi-Locale (motivation/tipp/etc.)
User-Bug: Admin-getriggerter Lyra-Post (Topic 'motivation' o.ä.) zeigte
trotz FR-locale im Frontend immer Deutsch. Root: LYRA_SYSTEM_PROMPT hatte
hartcodiert 'Auf Deutsch' → Groq output war immer DE.

Selbe Multi-Locale-Pattern wie approve.post.ts:
- 4 parallele Groq-Calls (Promise.allSettled) für de/en/fr/ar
- System-Prompt nimmt Lang-Parameter: 'Antwort AUSSCHLIESSLICH in <lang>'
- Content als JSON {de,en,fr,ar} gespeichert
- customContent-Path (Admin tippt selbst Text) bleibt plain — Admin schreibt
  in seiner gewählten Sprache, PostCard zeigt allen Usern denselben Text

Frontend (PostCard.tsx) parsed JSON bereits richtig via
resolveLocalizedJsonContent (vorheriger Commit 44a3348).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:44:05 +02:00
chahinebrini
44a3348845 feat(community): Domain-Approval-Lyra-Posts multi-locale (de/en/fr/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>
2026-05-18 00:29:02 +02:00
chahinebrini
fe2096309f fix(auth): Apple-Logo weiß in OAuth-Button
Vorher: fill='#0a0a0a' (schwarz) auf bg-neutral-900 (schwarzem Button) →
Logo unsichtbar. Erste TestFlight-Build (v0.3.0) hatte das noch drin —
Fix für v0.3.0-rebuild oder v0.3.1 hotfix.

Beide AppleIcon-Komponenten in signin.tsx + signup.tsx lokal dupliziert
(nicht in shared component) → beide separat editiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:24:10 +02:00
chahinebrini
23cc147231 feat(auth/ios): native Apple Sign-In via expo-apple-authentication
Vorher: stores/auth.ts hatte TODO + fiel auf Supabase-Web-OAuth-Flow zurück,
was fehlschlug mit 400 'Unsupported provider: missing OAuth client ID' weil
der Supabase-Apple-OAuth-Provider nicht konfiguriert ist.

Jetzt: native Flow ohne Supabase-Provider-Config —
- expo-apple-authentication.signInAsync() → identityToken
- supabase.auth.signInWithIdToken({provider:'apple', token}) verifiziert direkt
  gegen Apple's Public-Keys (kein Client-Secret-JWT-Setup nötig)
- User-Cancel (ERR_REQUEST_CANCELED) → leeres Resultat statt Error
- Platform-Guard: Apple-Path nur auf iOS

app.config.ts: ios.usesAppleSignIn=true → Expo prebuild generiert das
com.apple.developer.applesignin-Entitlement in die .entitlements. Beim
ersten EAS-Build wird die Capability auto-registriert im Apple-Developer-
Portal für org.rebreak.app.

signin.tsx + signup.tsx: Apple-Button conditional auf Platform.OS==='ios'
gerendert. Android-User sehen nur Google-Button (auf Android gibt es kein
natives Apple Sign-In).

App-Store-Submission-Pflicht (Apple Guideline 4.8 — wer OAuth-Login mit
3rd-Party-Provider anbietet, muss auch Apple Sign-In bieten).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:13:45 +02:00
chahinebrini
534f978b4e fix(onboarding): FAQ-answers + iOS-screen-time pointer alignment
FAQ-Bug-Fix + Component-Extraction:
- DoneSlide nutzte qkey.replace('q','a') → 'faq_q1'.replace('q','a')='faa_q1'
  weil .replace nur das ERSTE q matched (in "fa**q**"), nicht das in "q1".
  → Antworten resolved gegen non-existent key, raw key gerendert.
- Fix: explizite ID-Array [1,2,4,5,8] mit `help.faq_q\${id}` / `help.faq_a\${id}`.
- Shared FaqAccordion-Component extrahiert (components/FaqAccordion.tsx)
  mit 2 Varianten: 'card' (help/faq.tsx) + 'pills' (DoneSlide inline).
- app/help/faq.tsx + DoneSlide nutzen jetzt beide den shared component.

ScreenshotPointer-Alignment für iOS Screen-Time-Permission:
- iOS Family-Controls-Dialog: "Continue/Continuer/Fortfahren" ist LINKS-grau,
  "Don't Allow" ist RECHTS-blau (Apple platziert decline prominent, accept
  zurückhaltend bei Screen-Time-Permission). Pointer muss daher nach LINKS,
  nicht zentriert wie beim NEFilter-Dialog.
- ScreenshotPointer: neuer alignment-Prop ('left'|'center'|'right') →
  translateX (-80|0|+80 dp).
- ProtectionSlide iOS Phase B: pointerAlignment="left" durchgereicht.
- Phase A (url_filter) + alle Android-Phasen bleiben center.

Release-Prep (zied):
- CHANGELOG.md v0.3.0-Block erweitert (TTS, Stripe-Pricing, Keyboard-Fix,
  Single-Banner, FAQ-Extraktion, i18n-Status, Backend-Pending-Migration).
- version 0.3.0 + buildNumber 10 + versionCode 10 schon vorher gesetzt.
- eas.json production-Profil ready; Android-serviceAccountKeyPath bleibt
  TODO (User-Action: Google-Cloud-Service-Account anlegen).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:59:56 +02:00
chahinebrini
2e409efaf0 feat(onboarding/android + backend/lyra-i18n): platform-dispatch + post-catalog scaffold
Android-Onboarding (Platform.OS dispatch in ProtectionSlide):
- Neue Phasen für Android: preexplain_vpn → preexplain_a11y → a11y_pending
- AppState-Listener: nach Settings-Rückkehr auto-poll isAccessibilityEnabled
  → wenn live, armTamperLock + finish (kein Fokus-Klick nötig)
- onboardingAssets: 8 neue Mappings (android_vpn + android_a11y × 4 Locales)
- Screenshots: vpn-permission + a11y-rebreak-row pro Locale
- Locale-Keys: protection_url_android, protection_lock_android, cta_open_a11y,
  cta_check_a11y, dialog_button_vpn_ok, dialog_button_a11y_toggle, tap_marker_hint_*

Lyra-Post i18n Phase 1 (Scaffold, feature-flag OFF by default):
- schema.prisma: CommunityPost.i18nKey String? (nullable)
- migration 20260517_add_lyra_post_i18n_key: ALTER TABLE ADD COLUMN i18n_key
  (NICHT auto-deployed — `prisma migrate deploy` als separater Step)
- server/lib/lyraPostCatalog.ts: 15 Templates skelettiert + pickRandomTemplate
- cron/lyra-post: USE_TEMPLATE_CATALOG=true Branch → speichert i18nKey;
  default false → LLM-Path unverändert (zero-risk-deployment)
- community.createPost: optionaler i18nKey-Parameter
- posts.get: i18nKey in API-Response
- PostCard: 3-Zeilen-Branch — i18nKey ? t('lyra_posts.'+id) : content
- stores/community: i18nKey?: string|null im Interface
- de.json: lyra_posts-Block mit 15 IDs + DE-Texten

Single-Banner-Verhalten auf Android verifiziert:
lockedIn=urlFilter && appDeletionLock funktioniert weiter — auf Android
alias appDeletionLock ← tamperLock; onboarding arms tamperLock, also
nach onboarding-done direkt ProtectionLockedCard sichtbar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:48:25 +02:00
chahinebrini
ac605dce33 feat(onboarding,diga): TTS auto-play preference + 90 more DiGA test codes
## 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>
2026-05-17 22:39:18 +02:00
chahinebrini
56bb59915d feat(debug,protection): Force Reset for Android screenshot-capture
Bug-context: user reports nach Cooldown-Disable auf v0.2.1 Android-Build
reactiviert sich Schutz auto → a11y-Settings bleibt blockiert → keine Screenshots
möglich. v0.3.0 hat den Backend-protectionDisabledAt-Guard der das verhindert,
aber Test-Devices brauchen ein direktes Reset-Tool für Multi-Locale-Screenshots.

Backend:
- POST /api/protection/dev-force-disabled — sets protectionDisabledAt=NOW()
  ohne Cooldown-Vorlauf. Production-Guard (rebreak.org-non-staging → 403).

Frontend:
- /debug Android-Section refactored: "Force Reset + Settings öffnen" Button
- Bundle aus 3 Steps:
  1. native forceDisable (VPN stop + tamper disarm + filter_enabled=false)
  2. backend dev-force-disabled (Anti-Auto-Reactivation-Mark)
  3. Settings → Bedienungshilfen öffnen
- Danach: User toggled ReBreak-Service in Android-Settings manuell off
  → frischer a11y-deep-link-Trigger für nächste Screenshot-Iteration

Also: fix /onboarding/welcome → /onboarding (Duo-Rewrite hat den alten Pfad
gelöscht). Route 404 auf Android sichtbar wenn User in debug-toggle 'welcome'
oder 'nickname' tappt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:33:40 +02:00
chahinebrini
77bb7b84dc fix(keyboard): Android keyboard covers input — use react-native-keyboard-controller
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>
2026-05-17 21:58:05 +02:00
chahinebrini
c9273a2770 chore(release): v0.3.0 — version bump for TestFlight + Play Console
- package.json: 0.2.0 → 0.3.0
- app.config.ts: version 0.2.1 → 0.3.0
- iOS buildNumber: 9 → 10
- Android versionCode: 9 → 10
- CHANGELOG.md: v0.3.0 entry with Duo-Onboarding, DiGA, Stripe-pivot, Arabic+RTL,
  NEFilter-robust-disable, anti-auto-reactivation, FC always-on, etc.

Note: Android-Build wird vorerst NICHT submittet — Onboarding-Slides müssen für
Android-Protection-Mechanismus (VPN + a11y statt iOS NEFilter + Family Controls)
mit eigenen Pre-Explainer-Screenshots + Texten angepasst werden. Erst dann
v0.3.1 oder gesammelt mit Android.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:47:58 +02:00
chahinebrini
cfb0888451 fix(backend/staging): add NITRO_-prefix aliases for Stripe secrets
Bug: /api/stripe/checkout returned "STRIPE_SECRET_KEY fehlt" obwohl Var in
Infisical gesetzt war. Root: Nitro's useRuntimeConfig liest process.env zur
BUILD-Zeit — Stripe-Keys waren beim letzten Build nicht da. Runtime-Override
geht nur via NITRO_-Prefix env-var.

start-staging.sh re-exporten Stripe-Keys mit NITRO_-Prefix damit nitro's
useRuntimeConfig sie zur Laufzeit picked up:
  STRIPE_SECRET_KEY        → NITRO_STRIPE_SECRET_KEY
  STRIPE_WEBHOOK_SECRET    → NITRO_STRIPE_WEBHOOK_SECRET
  STRIPE_PUBLISHABLE_KEY   → NITRO_PUBLIC_STRIPE_PUBLISHABLE_KEY

(Pattern aus bestehender Liste — SUPABASE, DEEPGRAM, CARTESIA etc. nutzen
die gleiche Convention.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:43:02 +02:00
chahinebrini
22385d7d67 feat(stripe,onboarding): tier-rename + TTS audio button in lyra bubble
## Stripe Checkout Rename

Alte Legacy-Tier-Namen 'standard/pro' (von alter Tier-Struktur) waren
irreführend — heute heißt es 'pro/legend'. Cleanup:

- ENV-Var-Namen: STRIPE_PRICE_<PLAN>_<BILLING> (computed) statt
  hardcoded STANDARD/PRO Mapping. Erwartet:
    STRIPE_PRICE_PRO_MONTHLY
    STRIPE_PRICE_PRO_YEARLY
    STRIPE_PRICE_LEGEND_MONTHLY
    STRIPE_PRICE_LEGEND_YEARLY
- 'quarterly' billing entfernt (Strategist-Verdict: nur monthly + yearly,
  '2 Monate gratis' bei yearly).
- metadata enthält jetzt billing zusätzlich zu plan.

Webhook-Audit: bereits korrekt (mapped session.metadata.plan → pro/legend/free
via simple switch).

User-Action benötigt (Stripe Test-Dashboard):
- 4 Products + Prices anlegen mit 14-Tage-Trial
- Pricing pro Strategist: Pro 3,99/Mo + 39,90/Yr (2mo gratis),
  Legend 7,99/Mo + 79,90/Yr
- Webhook-Endpoint: https://staging.rebreak.org/api/stripe/webhook
  (Events: checkout.session.completed, customer.subscription.{updated,deleted})
- ENV-Vars (incl. STRIPE_WEBHOOK_SECRET) in Infisical pflegen

## TTS Audio-Button in LyraBubble

DiGA-Accessibility: Screen-Reader-Alternative + Lese-Hürden-Mitigation.

- lib/lyraSpeech.ts: one-shot TTS-Helper (vereinfacht aus SosTtsQueue)
  - Fetch /api/coach/speak mit Auth-Token
  - Bytes → Base64 → temp-file → expo-av Audio.Sound
  - Stop-fn: abortet in-flight fetch + unloaded sound
  - Status-callback: idle | loading | playing
- LyraBubble: Audio-Button rechts oben (orange Pill, 34×34)
  - Icon: volume-medium / hourglass / stop je nach status
  - Auto-stop bei text-change (Slide-Switch) + unmount
  - A11y-Labels in 4 Sprachen (audio_play / audio_loading / audio_stop)

Bubble-paddingRight erhöht auf 50 für Button-Platz.

## Locales

de/en/fr/ar: onboarding.lyra.audio_play / audio_loading / audio_stop

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:51:11 +02:00
chahinebrini
3c5c9ebfba feat(onboarding): polish bundle — nickname validation, diga format, confetti, FAQ accordion, lyra-voice tuned
## Nickname-Validation + Duplicate-Check

Bug-Prevention: User konnte einen bereits vergebenen Nickname setzen, was
zu Verwirrung führte (zwei User mit selbem Alias). + Profanity-Filter.

Backend:
- GET /api/profile/check-nickname?nickname=X — returns {available, reason?}
  reasons: 'too_short' | 'too_long' | 'profanity' | 'taken'
- Min 3, max 32 chars
- Profanity-Set (hardcoded, ~20 Wörter DE/EN — slurs + bot-impersonation
  wie "admin", "lyra", etc.)
- Case-insensitive lookup, ignoriert eigenen Nickname (= behalten ok)
- Soft-deleted Profile sind ausgeschlossen

Frontend:
- NicknameSlide refactored mit Live-Debounce (450ms)
- Race-guard via checkSeqRef damit veraltete Antworten verworfen werden
- Visueller Feedback: Border-Color (success/error/transparent), Status-
  Icon im Input (hourglass/checkmark/X), inline Error-Text statt Alert
- Save-Button disabled wenn invalid
- Network-Error: fail-soft, lass Server-Side bei Save validieren

## DiGA-Code Auto-Format

Live-Format-Mask: User tippt "REBREAKTEST001" → wird zu "REBREAK-TEST-001"
beim Tippen. Strip-then-segment Logik:
  1. Alles außer A-Z0-9 entfernen
  2. Erste 7 chars = "REBREAK", Rest in 4+restliche Blöcke

Liberal — erlaubt User dashes händisch zu setzen (wird neu segmentiert).

## DoneSlide Confetti + FAQ

- Confetti-Overlay mit 22 Partikeln, gestaffelt 40ms, native-driver Animation
  (translateY + drift + rotate + opacity fade). One-shot beim Mount.
- Inline Top-5-FAQ Accordion unter dem Checkmark-Hero. Tap auf row → expand
  + zeige Antwort. Nutzt existing help.faq_q1..q5 + .faq_a1..a5 locale keys.

## Lyra Voice-Review (Agent)

lyra-persona Agent hat alle Lyra-Speech-Texte in 4 Sprachen reviewed:
- Welcome entstigmatisiert (kein "Glücksspiel"-Trigger im First-Touch)
- Plan vermenschlicht (Erklärungs- statt Verkaufs-Ton)
- DiGA-Choice sanfter (Geschenk-Frame statt Zugangs-Frame)
- protection_lock parallelisiert mit "blaue Falle"-Warnung
- FR/AR Stilglättung (Lyra-Femininum konsistent, AR Frage-Forms)

## Locale-Additions

- onboarding.nickname.error_{too_short, too_long, profanity, taken} × 4 langs
- onboarding.done.faq_section_title × 4 langs
- Lyra-bodies × 4 langs (vom Agent getuned)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:09:53 +02:00
chahinebrini
33aa3464b8 feat(onboarding): protection pointer redesign + i18n screenshots + lockedIn fix
## 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>
2026-05-17 19:58:56 +02:00
chahinebrini
1596a4ea7a feat(protection,onboarding): anti-auto-reactivation + protection pre-explainer + custom sheets
## 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>
2026-05-17 19:05:37 +02:00
chahinebrini
b23bd6d29f feat(onboarding,protection): Duo-style flow + cooldown auto-disable fix + Family Controls live
## Duo-Style Onboarding (Foundation + alle Slides)

Self-contained Onboarding-Flow mit Lyra-Mascot ersetzt das Spotlight-POC vom
vorherigen Iteration. Slides leben unter `components/onboarding/slides/`.

- Foundation: OnboardingShell (Progress + ScrollView + sticky CTABar),
  LyraBubble (Rive-Avatar + animierte Speech-Bubble), SlideProgress, CTABar
- Slides: Welcome, Privacy (4 Versprechen), Nickname (inline + PATCH /me),
  DigaChoice (Ja/Nein-Branch), DigaCode (redeem-Endpoint + inline-Errors),
  Plan (Pro/Legend cards, monthly/yearly toggle, 2 Monate gratis, Härtefall-
  Mailto), Payment (RevenueCat-Dev-Stub bis Phase-0), Protection (activate +
  PermissionDeniedSheet-Wiring), Done (animierter Checkmark + Streak-Day-1)
- State-Machine in app/onboarding/index.tsx: 9 Slides, DiGA-Branch, Resume-
  on-launch via slideFromStep(me.onboardingStep)
- Routing-gate in (app)/_layout.tsx: step != 'done' → /onboarding
- Backend Profile.onboardingStep enum extended:
  welcome | account | plan | pre_protection | done (+ legacy nickname/block)
- Backend diga redeem: step='pre_protection' (NICHT 'done') — User muss noch
  durch Protection-Slide für NEFilter/VPN-Aktivierung
- Locale-Keys (de/en/fr/ar): onboarding.lyra.<slide>.body, .cta_primary,
  Plan-Tier-Details (3,99/7,99 €/Mo, 39,90/79,90 €/Jahr mit 2 Monaten gratis),
  Härtefall-Link, DiGA-Code-Errors, Protection-Feat-Descriptions

## Cooldown Auto-Disable Race-Fix

Bug: nach Cooldown-Ablauf bleib URL-Filter installiert (NEFilter in iOS-
Settings sichtbar als "Läuft..."). Root-cause: `/api/cooldown/status` GET
auto-resolved beim ersten expired-Hit; zweiter Call in
applyCooldownDisableIfElapsed sah cooldownEndsAt=null → bail → forceDisable
nie aufgerufen.

- useProtectionState.fetchState: lokalen next.cooldown.endsAt state nutzen
  statt redundantem API-Call. Atomarer, race-frei.
- AppState-Listener-Path unverändert (dort ist es der erste API-Call, kein
  Race).
- lib/protection.forceDisable: console.log für Debug-Visibility.

## iOS NEFilter Robust-Disable (Native)

`removeFromPreferences()` alleine ist auf iOS 18+ unzuverlässig — Settings-
UI zeigt "Läuft..." obwohl Provider beendet sein sollte. 2-Step-Pattern:

  1. loadFromPreferences
  2. isEnabled = false + saveToPreferences (stoppt Filter-Daemon)
  3. removeFromPreferences (Config-Eintrag aus Settings)

Quelle: Apple-Developer-Forums + eigene Empirie. Pattern wird auch in
PermissionDeniedSheet's resetUrlFilter genutzt (analog).

## Family Controls jetzt immer aktiv

Apple-Entitlement seit 2026-05 für ReBreak approved (TestFlight-akzeptiert).
`familyControlsEnabled: true` hart in app.config.ts (kein Env-Var-Gating mehr).
"Bald verfügbar"-Placeholder in blocker.tsx entfernt — App-Lock-Toggle ist
jetzt voll funktional auf iOS.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:48:05 +02:00
chahinebrini
ae92918449 refactor(onboarding): drop spotlight, prepare for Duo-style flow
Spotlight-on-real-UI Approach wurde verworfen zugunsten eines Duolingo-style
Onboardings (Lyra als Mascot, self-contained Slides für jeden Step). Strategisch
ausgelöst durch den Pricing-Pivot (Free → nur Pro/Legend mit 14-Tage-Trial),
weil Free-Drop + Trial + DiGA-Code-Branch + RevenueCat-IAP nicht mit Spotlight-
auf-Real-UI vereinbar sind.

Removed:
- components/OnboardingHint.tsx (Tooltip + Glow Reanimated/Animated POC)
- Spotlight wiring in app/profile/edit.tsx (header step-progress, save-handler
  routing zu /(app)/blocker, onboarding-aware Back-Hide, Tooltip + Glow wrappers
  ums Nickname-Input)
- Spotlight wiring in app/(app)/blocker.tsx (useMe-Import, onboardingActive,
  stepCompletedRef, Auto-PATCH-Effect, Tooltip + Glow um LayerSwitchCard)
- Routing-gate Nickname-Branch in app/(app)/_layout.tsx
- react-native-copilot dependency aus package.json + lockfile

Kept:
- Backend onboarding-step state machine (wird im Duo-Flow weiter genutzt)
- Welcome-Screen app/onboarding/welcome.tsx (wird Slide 1 des neuen Flows)
- useMe.onboardingStep type
- Avatar-Bug-Fix in profile/edit (Dicebear-Seed stabil beim Tippen)
- onSubmitEditing auto-save in TextInput (orthogonale UX-Verbesserung)
- Routing-gate Welcome-Branch (step != 'done' → /onboarding/welcome)
- Debug-Reset-Toggle, Arabic locale + RTL, PermissionDeniedSheet, Swift
  resetUrlFilter (alles orthogonal)
- Locale-Keys onboarding.welcome.*, step_progress, nickname_spotlight.*,
  block_spotlight.* (werden ggf. im Duo-Flow neu-gemapped)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:00:12 +02:00
chahinebrini
68074aa7b7 feat(diga): redeem-code endpoint + 10 test codes seeded
DiGA-Pfad-Foundation: User mit Rezept-Code löst im Onboarding ein, wird auf
plan='legend' hochgestuft (Default), Onboarding-Step springt auf 'done',
diga_code_redeemed_at als Audit-Trail. Trial-Modell wird übersprungen.

- Prisma model DigaCode (code unique, expires_at, used_at, used_by_profile_id,
  grants_plan, notes, label)
- Profile.digaCodeRedeemedAt für Reverse-Audit
- Migration 20260517_add_diga_codes mit Table + FK + Index
- Seed: REBREAK-TEST-001..010 (single-use, reset via SQL für erneutes Testen)
- POST /api/onboarding/redeem-diga-code — atomare Transaction, klare 400-Errors
  (not_found | already_used | expired | invalid_input)

Frontend (Duo-Onboarding) dockt später an — diese Backend-Foundation steht.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 15:52:53 +02:00
chahinebrini
1c9e67c256 feat(onboarding,protection,i18n): spotlight POC, arabic locale, NEFilter recovery
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>
2026-05-17 15:44:32 +02:00
chahinebrini
38a8517259 feat(onboarding): interactive welcome + nickname spotlight tour
Stage 1+2 des post-signup Onboarding-Flows:
- Welcome-Screen: dark-slate Full-Screen mit Pulse-Hero, 3 Mission-Bullets,
  DSGVO-Box, CTA "Los geht's"
- Nickname-Spotlight via react-native-copilot ums TextInput in /profile/edit,
  auto-start wenn step='nickname', nach Save → step='block' + back to /(app)
- Backend: Profile.onboardingStep enum (welcome/nickname/block/done),
  Migration mit Backfill (existing → done), PATCH /api/profile/me/onboarding-step,
  /api/auth/me erweitert
- Frontend: CopilotProvider in root, Routing-Gate in (app)/_layout, useMe um
  onboardingStep ergänzt
- i18n (de/en/fr) für onboarding.welcome.* + onboarding.nickname_spotlight.*

Stage 3 (Block-Aktivierung-Spotlight) folgt in nächster Session — der bestehende
ProtectionOnboardingSheet auf Android wird daran angebunden.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:00:20 +02:00
chahinebrini
c1dd7e7320 fix(native/protection-android): a11y plugin self-heals XML, arm tamper-lock on return, truthful status check
- 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>
2026-05-16 11:24:45 +02:00
chahinebrini
83b0d7a062 feat(native/protection): android a11y banner status + 2-step onboarding sheet
- 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>
2026-05-16 10:34:48 +02:00
chahinebrini
6e34631246 feat(db): temporary default plan=legend while tier toggle is missing
UPDATE all existing profiles to legend + ALTER COLUMN default to legend
so internal testers can exercise premium paths until the settings toggle
is rebuilt. Revert via follow-up migration once the toggle is back.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 10:04:34 +02:00
chahinebrini
4124463097 chore(release): bump to v0.2.1 / versionCode 9 — theme-crash, double-splash, dm-reopen fixes
- Android Theme parent → Theme.MaterialComponents.DayNight.NoActionBar.Bridge
  (fix BadgeDrawable crash in react-native-bottom-tabs after AccessibilityService toggle)
- Plugin with-material-theme-android keeps theme idempotent across prebuilds
- Plugin with-release-signing-android wires release signingConfig from key.properties
- Splash: align native splash image with JS BrandSplash (icon.png) to eliminate
  double-splash flicker on app start
- DM: reset partner/messages/replyTo state on userId change, disable cache for
  history query, switch spinner condition to isLoading||isFetching so reopens
  always load fresh and never show empty-state with stale partner

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:32:28 +02:00