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>
This commit is contained in:
parent
77bb7b84dc
commit
56bb59915d
@ -7,6 +7,7 @@ import {
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
Clipboard,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
@ -15,7 +16,7 @@ import { useColors } from '../lib/theme';
|
||||
import { useMe, invalidateMe, type Plan } from '../hooks/useMe';
|
||||
import { apiFetch } from '../lib/api';
|
||||
import { PlanChangeSheet } from '../components/plan/PlanChangeSheet';
|
||||
import { getCooldownTestMode, setCooldownTestMode } from '../lib/protection';
|
||||
import { getCooldownTestMode, setCooldownTestMode, protection } from '../lib/protection';
|
||||
import { useRealtimeDebugStore, type LogEntry } from '../stores/realtimeDebug';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
@ -115,6 +116,8 @@ export default function DebugScreen() {
|
||||
|
||||
<CooldownTestModeToggle />
|
||||
|
||||
{Platform.OS === 'android' ? <AndroidA11yResetToggle colors={colors} /> : null}
|
||||
|
||||
<RealtimeStatusCard />
|
||||
<RealtimeLogCard />
|
||||
|
||||
@ -690,9 +693,13 @@ function OnboardingResetToggle({
|
||||
});
|
||||
invalidateMe();
|
||||
if (step === 'welcome') {
|
||||
router.replace('/onboarding/welcome');
|
||||
// /onboarding/welcome existiert nicht mehr (Duo-Rewrite → /onboarding/index.tsx).
|
||||
// Korrekter Pfad ist /onboarding (Expo Router löst index.tsx auto auf).
|
||||
router.replace('/onboarding');
|
||||
} else if (step === 'nickname') {
|
||||
router.replace('/profile/edit');
|
||||
// Legacy-Stage — Duo-Flow navigiert intern; /onboarding triggert via
|
||||
// slideFromStep den Resume zur Nickname-Slide.
|
||||
router.replace('/onboarding');
|
||||
} else if (step === 'block') {
|
||||
router.replace('/(app)/blocker');
|
||||
} else if (step === 'done') {
|
||||
@ -783,6 +790,117 @@ function OnboardingResetToggle({
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Android: A11y-Settings Quick-Open ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Android-only Dev-Helper: öffnet Settings → Bedienungshilfen damit User den
|
||||
* ReBreak-Service manuell off-toggeln kann. Wird gebraucht für Screenshot-
|
||||
* Capture vom a11y-Settings-Page in 4 Sprachen — nach Cooldown disarmt
|
||||
* forceDisable() den tamper-lock, aber das System-Switch in den Einstellungen
|
||||
* bleibt programmatisch un-modifizierbar (Android-OS-Restriction).
|
||||
*/
|
||||
function AndroidA11yResetToggle({
|
||||
colors,
|
||||
}: {
|
||||
colors: import('../lib/theme').ColorScheme;
|
||||
}) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function forceResetAndOpen() {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
// Step 1: native forceDisable — stoppt VPN, disarmt tamper-lock, setzt
|
||||
// filter_enabled=false → a11y-service wird passiv → blockt nichts mehr.
|
||||
await protection.forceDisable();
|
||||
|
||||
// Step 2: backend protection-state auf "explizit disabled" markieren →
|
||||
// enforceProtection-Loop feuert KEINE Auto-Reactivation in den nächsten
|
||||
// Pollings. Sonst wäre der a11y-Service-Off-Toggle zwecklos weil die
|
||||
// App den Filter sofort wieder hochfährt.
|
||||
await apiFetch('/api/protection/dev-force-disabled', { method: 'POST' });
|
||||
invalidateMe();
|
||||
|
||||
// Step 3: Android-Settings → Bedienungshilfen öffnen — User toggelt
|
||||
// ReBreak-Service manuell off (Android-OS-Restriction: programmatisch
|
||||
// nicht setzbar). Danach Screenshot-Capture frischer Trigger möglich.
|
||||
await protection.openSystemSettings('accessibility');
|
||||
} catch (e: unknown) {
|
||||
Alert.alert('Fehler', e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0,0,0,0.05)',
|
||||
padding: 14,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 11,
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Ionicons name="accessibility-outline" size={18} color={colors.textMuted} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
|
||||
Force Reset (Android)
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: colors.textMuted,
|
||||
fontFamily: 'Nunito_400Regular',
|
||||
marginTop: 3,
|
||||
lineHeight: 17,
|
||||
}}
|
||||
>
|
||||
VPN stoppen + tamper-lock disarmen + Backend als disabled markieren
|
||||
+ Settings öffnen — Anti-Auto-Reactivation für Screenshot-Capture.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={forceResetAndOpen}
|
||||
disabled={busy}
|
||||
activeOpacity={0.7}
|
||||
style={{
|
||||
backgroundColor: colors.brandOrange,
|
||||
borderRadius: 10,
|
||||
paddingVertical: 10,
|
||||
alignItems: 'center',
|
||||
opacity: busy ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontFamily: 'Nunito_700Bold',
|
||||
color: '#ffffff',
|
||||
}}
|
||||
>
|
||||
{busy ? 'Reset läuft...' : 'Force Reset + Settings öffnen'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Cooldown Test Mode ────────────────────────────────────────────────────
|
||||
|
||||
function CooldownTestModeToggle() {
|
||||
|
||||
37
backend/server/api/protection/dev-force-disabled.post.ts
Normal file
37
backend/server/api/protection/dev-force-disabled.post.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { requireUser } from "../../utils/auth";
|
||||
import { usePrisma } from "../../utils/prisma";
|
||||
|
||||
/**
|
||||
* POST /api/protection/dev-force-disabled
|
||||
*
|
||||
* DEV/STAGING-ONLY: Setzt protectionDisabledAt = NOW() ohne Cooldown-Vorlauf.
|
||||
* Frontend-Debug-Button für Screenshot-Capture (Android-a11y-reset-flow).
|
||||
*
|
||||
* Production-Guard: appUrl enthält "rebreak.org" aber NICHT "staging" → 403.
|
||||
*
|
||||
* Sobald gesetzt:
|
||||
* - /api/protection/state gibt protectionShouldBeActive=false zurück
|
||||
* - Frontend's enforceProtection-Loop feuert KEINE Auto-Reactivation mehr
|
||||
* - User kann a11y-Settings öffnen und manuell den ReBreak-Service off-toggeln
|
||||
*
|
||||
* Zum Wiedereinschalten: POST /api/protection/mark-active (clear flag).
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = await requireUser(event);
|
||||
|
||||
const config = useRuntimeConfig(event);
|
||||
const appUrl = (config.public?.appUrl as string) ?? "";
|
||||
const isProductionUrl =
|
||||
appUrl.includes("rebreak.org") && !appUrl.includes("staging");
|
||||
if (isProductionUrl) {
|
||||
throw createError({ statusCode: 403, message: "dev-only" });
|
||||
}
|
||||
|
||||
const db = usePrisma();
|
||||
await db.profile.update({
|
||||
where: { id: user.id },
|
||||
data: { protectionDisabledAt: new Date() },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user