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

79 lines
2.2 KiB
TypeScript

import { requireUser } from "../../utils/auth";
import { getActiveCooldown, resolveCooldown } from "../../db/cooldown";
import { signCooldownToken } from "../../utils/cooldownToken";
import { usePrisma } from "../../utils/prisma";
/** GET /api/cooldown/status — Current cooldown state for the authenticated user. */
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const cooldown = await getActiveCooldown(user.id);
const now = new Date();
if (!cooldown) {
// No cooldown ever started (or all were cancelled/resolved).
return {
success: true,
data: {
active: false,
remainingSeconds: 0,
cooldownEndsAt: null,
canDisableProtection: true,
token: null, // no cooldown row to bind to; app may proceed freely
},
};
}
const expired = now >= cooldown.cooldownEndsAt;
if (expired) {
// Auto-resolve so we don't re-check next time.
await resolveCooldown(cooldown.id);
// Anti-Auto-Reactivation: User hat den 24h-Cooldown durchgehalten + jetzt
// wird der Schutz abgeschaltet. Markiere Profile damit /api/protection/state
// protectionShouldBeActive=false zurückgibt → Frontend macht keine Auto-
// Reactivation (Sucht-Recovery-Pattern: einfach an, schwer aus, sehr schwer
// wieder zurück an Auto-Magic). User muss explizit reaktivieren.
const db = usePrisma();
await db.profile.update({
where: { id: user.id },
data: { protectionDisabledAt: new Date() },
}).catch(() => {});
const token = await signCooldownToken(
user.id,
cooldown.tokenJti,
cooldown.cooldownEndsAt,
);
return {
success: true,
data: {
active: false,
remainingSeconds: 0,
cooldownEndsAt: cooldown.cooldownEndsAt.toISOString(),
canDisableProtection: true,
token,
},
};
}
// Still counting down.
const remainingSeconds = Math.max(
0,
Math.floor((cooldown.cooldownEndsAt.getTime() - now.getTime()) / 1000),
);
return {
success: true,
data: {
active: true,
remainingSeconds,
cooldownEndsAt: cooldown.cooldownEndsAt.toISOString(),
canDisableProtection: false,
token: null,
},
};
});