## 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>
61 lines
2.1 KiB
TypeScript
61 lines
2.1 KiB
TypeScript
/**
|
|
* iOS Permission-Dialog-Screenshots für den Onboarding-Pre-Explainer.
|
|
*
|
|
* Pro Sprache liegen die Screenshots in `assets/onboarding/<lang>/`. Falls
|
|
* eine Sprache nicht (oder noch nicht) verfügbar ist, fällt der Resolver auf
|
|
* `de` zurück.
|
|
*
|
|
* Diese Maps explizit mit `require(...)` deklarieren — RN/Metro kann keine
|
|
* dynamischen Pfade auflösen.
|
|
*/
|
|
|
|
type Dialog = 'url_filter' | 'screen_time';
|
|
type Lang = 'de' | 'en' | 'fr' | 'ar';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const URL_FILTER_DE = require('../assets/onboarding/de/url_filter_permission.jpeg');
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const SCREEN_TIME_DE = require('../assets/onboarding/de/screen_time_permission.jpeg');
|
|
|
|
// TODO: en / fr / ar Screenshots ergänzen sobald User uploaded:
|
|
// require('../assets/onboarding/en/url_filter_permission.jpeg'), etc.
|
|
const SCREENSHOTS: Record<Dialog, Partial<Record<Lang, number>>> = {
|
|
url_filter: {
|
|
de: URL_FILTER_DE,
|
|
},
|
|
screen_time: {
|
|
de: SCREEN_TIME_DE,
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Resolves the right screenshot for the current language, with de-fallback.
|
|
* Returns the result of `require(...)` (an opaque module-handle for Metro).
|
|
*/
|
|
export function getPermissionScreenshot(dialog: Dialog, lang: string): number {
|
|
const normalized = (
|
|
lang === 'de' || lang === 'en' || lang === 'fr' || lang === 'ar' ? lang : 'de'
|
|
) as Lang;
|
|
const map = SCREENSHOTS[dialog];
|
|
return map[normalized] ?? map.de!;
|
|
}
|
|
|
|
/**
|
|
* Wo der Pointer-Marker auf dem Screenshot positioniert werden soll.
|
|
* Werte sind Prozent (0..100) relativ zum Container.
|
|
*
|
|
* Beide iOS-Dialoge haben den korrekten Button am Bottom (~85-88% Y, 50% X).
|
|
* Falls Apple das Layout pro Locale anders rendert — pro-Lang-Overrides
|
|
* hier ergänzen.
|
|
*/
|
|
export function getPointerPosition(dialog: Dialog): { xPercent: number; yPercent: number } {
|
|
switch (dialog) {
|
|
case 'url_filter':
|
|
// Button "Erlauben" am unteren Rand des Modals
|
|
return { xPercent: 50, yPercent: 86 };
|
|
case 'screen_time':
|
|
// Button "Fortfahren" am unteren Rand
|
|
return { xPercent: 50, yPercent: 86 };
|
|
}
|
|
}
|