RebreakVpnService.onStartCommand crashed with SecurityException because Android 16's validateForegroundServiceType rejects the implicit 2-arg startForeground(). Now passes FOREGROUND_SERVICE_TYPE_SPECIAL_USE explicitly (Google's documented best practice) and guards the call so a failed foreground promotion stops the service cleanly instead of crashing the app. Verified vs reported Galaxy A54 / Android 16 signature (97% of crash events, 1-user crash loop). Bundles pending working-tree work across native/marketing/locales/mac + graphify-out rebuild. gitignore: google-services.json + /screenshots/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
635 lines
22 KiB
TypeScript
635 lines
22 KiB
TypeScript
import { useState, type ReactNode } from "react";
|
|
import { View, Text, TouchableOpacity, ActivityIndicator } from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
|
|
// Geteilter Schutz-Setup-Flow (Reihenfolge + Gating). Quelle der Wahrheit für
|
|
// Blocker UND Onboarding — NIE die Reihenfolge hier ändern ohne beide zu prüfen.
|
|
// Android: VPN → Geräteadmin → a11y (a11y zuletzt, sonst blockt der Tamper-Lock
|
|
// die Admin-Seite). iOS: App-Lock → Bildschirmzeit → URL-Filter.
|
|
// ─── iOS Unsupervised 3-Step Setup Flow ──────────────────────────────────────
|
|
|
|
type SetupFlowProps = {
|
|
familyControlsActive: boolean;
|
|
screentimeCode: string | null;
|
|
screentimeConfirmed: boolean;
|
|
screentimeSaving: boolean;
|
|
urlFilterActive: boolean;
|
|
onActivateFamilyControls: () => Promise<{ enabled: boolean; error?: string }>;
|
|
onGenerateScreentimeCode: () => void;
|
|
onConfirmScreentime: () => void;
|
|
onActivateUrlFilter: () => Promise<{ enabled: boolean; error?: string }>;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
};
|
|
|
|
export function IosUnsupervisedSetupFlow({
|
|
familyControlsActive,
|
|
screentimeCode,
|
|
screentimeConfirmed,
|
|
screentimeSaving,
|
|
urlFilterActive,
|
|
onActivateFamilyControls,
|
|
onGenerateScreentimeCode,
|
|
onConfirmScreentime,
|
|
onActivateUrlFilter,
|
|
colors,
|
|
t,
|
|
}: SetupFlowProps) {
|
|
const step1Done = familyControlsActive;
|
|
const step2Done = screentimeConfirmed;
|
|
const step3Done = urlFilterActive;
|
|
|
|
return (
|
|
<View style={{ gap: 10 }}>
|
|
<SetupStep1
|
|
done={step1Done}
|
|
onActivate={onActivateFamilyControls}
|
|
colors={colors}
|
|
t={t}
|
|
/>
|
|
<SetupStep2
|
|
unlocked={step1Done}
|
|
code={screentimeCode}
|
|
confirmed={step2Done}
|
|
saving={screentimeSaving}
|
|
onGenerate={onGenerateScreentimeCode}
|
|
onConfirm={onConfirmScreentime}
|
|
colors={colors}
|
|
t={t}
|
|
/>
|
|
<SetupStep3
|
|
unlocked={step2Done}
|
|
done={step3Done}
|
|
onActivate={onActivateUrlFilter}
|
|
colors={colors}
|
|
t={t}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function SetupStep1({
|
|
done,
|
|
onActivate,
|
|
colors,
|
|
t,
|
|
}: {
|
|
done: boolean;
|
|
onActivate: () => Promise<{ enabled: boolean; error?: string }>;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handlePress() {
|
|
if (done || busy) return;
|
|
setBusy(true);
|
|
try { await onActivate(); } finally { setBusy(false); }
|
|
}
|
|
|
|
return (
|
|
<SetupStepCard
|
|
stepNumber={1}
|
|
title={t('blocker.setup_step1_title')}
|
|
subtitle={done ? t('blocker.setup_step1_subtitle_done') : t('blocker.setup_step1_subtitle_pending')}
|
|
done={done}
|
|
unlocked
|
|
colors={colors}
|
|
>
|
|
{!done && (
|
|
<TouchableOpacity
|
|
onPress={handlePress}
|
|
activeOpacity={0.8}
|
|
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center', marginTop: 10 }}
|
|
>
|
|
{busy
|
|
? <ActivityIndicator size="small" color="#fff" />
|
|
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.setup_step1_cta')}</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
)}
|
|
</SetupStepCard>
|
|
);
|
|
}
|
|
|
|
function SetupStep2({
|
|
unlocked,
|
|
code,
|
|
confirmed,
|
|
saving,
|
|
onGenerate,
|
|
onConfirm,
|
|
colors,
|
|
t,
|
|
}: {
|
|
unlocked: boolean;
|
|
code: string | null;
|
|
confirmed: boolean;
|
|
saving: boolean;
|
|
onGenerate: () => void;
|
|
onConfirm: () => void;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
}) {
|
|
return (
|
|
<SetupStepCard
|
|
stepNumber={2}
|
|
title={t('blocker.setup_step2_title')}
|
|
subtitle={confirmed ? t('blocker.setup_step2_subtitle_done') : t('blocker.setup_step2_subtitle_pending')}
|
|
done={confirmed}
|
|
unlocked={unlocked}
|
|
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 1 })}
|
|
colors={colors}
|
|
>
|
|
{unlocked && !confirmed && (
|
|
<View style={{ gap: 10, marginTop: 10 }}>
|
|
{!code ? (
|
|
<TouchableOpacity
|
|
onPress={onGenerate}
|
|
activeOpacity={0.8}
|
|
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center' }}
|
|
>
|
|
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>
|
|
{t('blocker.screentime_generate_cta')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
) : (
|
|
<View style={{ gap: 10 }}>
|
|
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 12, padding: 16, alignItems: 'center', gap: 4 }}>
|
|
<Text style={{ fontSize: 11, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted }}>
|
|
{t('blocker.screentime_code_label')}
|
|
</Text>
|
|
<Text style={{ fontSize: 36, fontFamily: 'Nunito_700Bold', color: colors.brandOrange, letterSpacing: 12 }}>
|
|
{code}
|
|
</Text>
|
|
</View>
|
|
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 12, padding: 12, gap: 6 }}>
|
|
{[
|
|
t('blocker.screentime_step1'),
|
|
t('blocker.screentime_step2'),
|
|
t('blocker.screentime_step3'),
|
|
].map((step, i) => (
|
|
<Text
|
|
key={i}
|
|
style={{
|
|
fontSize: 13,
|
|
fontFamily: i === 0 ? 'Nunito_700Bold' : 'Nunito_600SemiBold',
|
|
color: i === 0 ? colors.brandOrange : colors.text,
|
|
lineHeight: 18,
|
|
}}
|
|
>
|
|
{step}
|
|
</Text>
|
|
))}
|
|
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 16, marginTop: 2 }}>
|
|
{t('blocker.screentime_step_note')}
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={onConfirm}
|
|
disabled={saving}
|
|
activeOpacity={0.8}
|
|
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center' }}
|
|
>
|
|
{saving
|
|
? <ActivityIndicator size="small" color="#fff" />
|
|
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.screentime_confirm_cta')}</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
</SetupStepCard>
|
|
);
|
|
}
|
|
|
|
function SetupStep3({
|
|
unlocked,
|
|
done,
|
|
onActivate,
|
|
colors,
|
|
t,
|
|
}: {
|
|
unlocked: boolean;
|
|
done: boolean;
|
|
onActivate: () => Promise<{ enabled: boolean; error?: string }>;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handlePress() {
|
|
if (done || busy) return;
|
|
setBusy(true);
|
|
try { await onActivate(); } finally { setBusy(false); }
|
|
}
|
|
|
|
return (
|
|
<SetupStepCard
|
|
stepNumber={3}
|
|
title={t('blocker.setup_step3_title')}
|
|
subtitle={done ? t('blocker.setup_step3_subtitle_done') : t('blocker.setup_step3_subtitle_pending')}
|
|
done={done}
|
|
unlocked={unlocked}
|
|
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 2 })}
|
|
colors={colors}
|
|
>
|
|
{unlocked && !done && (
|
|
<View style={{ gap: 10, marginTop: 10 }}>
|
|
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 10, padding: 10, flexDirection: 'row', gap: 8 }}>
|
|
<Ionicons name="information-circle-outline" size={15} color={colors.textMuted} style={{ marginTop: 1 }} />
|
|
<Text style={{ flex: 1, fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 17 }}>
|
|
{t('blocker.setup_step3_warning')}
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={handlePress}
|
|
activeOpacity={0.8}
|
|
style={{ backgroundColor: colors.success, borderRadius: 10, paddingVertical: 10, alignItems: 'center' }}
|
|
>
|
|
{busy
|
|
? <ActivityIndicator size="small" color="#fff" />
|
|
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.setup_step3_cta')}</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</SetupStepCard>
|
|
);
|
|
}
|
|
|
|
// ─── Android 3-Step Setup Flow ───────────────────────────────────────────────
|
|
|
|
type AndroidSetupFlowProps = {
|
|
vpnActive: boolean;
|
|
// VPN-Aktivierung läuft → Spinner statt CTA, bis der Layer-State bestätigt ist.
|
|
vpnActivating?: boolean;
|
|
// = a11y-Service enabled UND Tamper-Lock armed (appDeletionLock). Nur "enabled"
|
|
// reicht NICHT — der a11y-Service ist ohne armed-Flag komplett passiv.
|
|
accessibilityLocked: boolean;
|
|
deviceAdminActive: boolean;
|
|
// Akku-Ausnahme (gegen Samsung-Sleep, der den a11y-Lock entbindet → Schutz weg).
|
|
batteryUnrestricted: boolean;
|
|
onActivateVpn: () => Promise<{ enabled: boolean; error?: string }>;
|
|
onActivateAccessibility: () => Promise<unknown>;
|
|
onRequestDeviceAdmin: () => Promise<{ launched: boolean }>;
|
|
onRequestBattery: () => Promise<{ opened: boolean; alreadyIgnored?: boolean }>;
|
|
onOpenAppDetails: () => Promise<{ opened: boolean }>;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
};
|
|
|
|
export function AndroidSetupFlow({
|
|
vpnActive,
|
|
vpnActivating,
|
|
accessibilityLocked,
|
|
deviceAdminActive,
|
|
batteryUnrestricted,
|
|
onActivateVpn,
|
|
onActivateAccessibility,
|
|
onRequestDeviceAdmin,
|
|
onRequestBattery,
|
|
onOpenAppDetails,
|
|
colors,
|
|
t,
|
|
}: AndroidSetupFlowProps) {
|
|
// Reihenfolge KRITISCH: VPN → Geräteadmin → Akku-Ausnahme → a11y. a11y MUSS
|
|
// zuletzt, weil der Tamper-Lock (sobald armed) Settings-Seiten blockt. Die
|
|
// Akku-Ausnahme MUSS VOR a11y: sonst schläfert Samsung den frisch aktivierten
|
|
// a11y-Service wieder ein → Lock entbunden → Schutz wertlos.
|
|
const vpnDone = vpnActive;
|
|
const adminDone = deviceAdminActive;
|
|
const batteryDone = batteryUnrestricted;
|
|
const a11yDone = accessibilityLocked;
|
|
|
|
return (
|
|
<View style={{ gap: 10 }}>
|
|
<AndroidStep1
|
|
done={vpnDone}
|
|
pending={!!vpnActivating && !vpnDone}
|
|
onActivate={onActivateVpn}
|
|
colors={colors}
|
|
t={t}
|
|
/>
|
|
{/* Display-Step 2 = Geräteadmin (Komponente AndroidStep3, i18n android_step3_*). */}
|
|
<AndroidStep3
|
|
unlocked={vpnDone}
|
|
done={adminDone}
|
|
onRequestAdmin={onRequestDeviceAdmin}
|
|
colors={colors}
|
|
t={t}
|
|
/>
|
|
{/* Display-Step 3 = Akku-Ausnahme (gegen Samsung-Sleep, der a11y entbindet). */}
|
|
<AndroidStepBattery
|
|
unlocked={adminDone}
|
|
done={batteryDone}
|
|
onRequest={onRequestBattery}
|
|
onOpenDetails={onOpenAppDetails}
|
|
colors={colors}
|
|
t={t}
|
|
/>
|
|
{/* Display-Step 4 = a11y / ReBreak-Schutz (Komponente AndroidStep2, i18n android_step2_*). */}
|
|
<AndroidStep2
|
|
unlocked={batteryDone}
|
|
done={a11yDone}
|
|
onActivate={onActivateAccessibility}
|
|
colors={colors}
|
|
t={t}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function AndroidStep1({
|
|
done,
|
|
pending,
|
|
onActivate,
|
|
colors,
|
|
t,
|
|
}: {
|
|
done: boolean;
|
|
pending?: boolean;
|
|
onActivate: () => Promise<{ enabled: boolean; error?: string }>;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handlePress() {
|
|
if (done || busy) return;
|
|
setBusy(true);
|
|
try { await onActivate(); } finally { setBusy(false); }
|
|
}
|
|
|
|
return (
|
|
<SetupStepCard
|
|
stepNumber={1}
|
|
title={t('blocker.android_step1_title')}
|
|
subtitle={done ? t('blocker.android_step1_subtitle_done') : t('blocker.android_step1_subtitle_pending')}
|
|
done={done}
|
|
unlocked
|
|
colors={colors}
|
|
>
|
|
{!done && (pending ? (
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 11, marginTop: 10 }}>
|
|
<ActivityIndicator size="small" color={colors.brandOrange} />
|
|
<Text style={{ fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted }}>{t('blocker.activating')}</Text>
|
|
</View>
|
|
) : (
|
|
<TouchableOpacity
|
|
onPress={handlePress}
|
|
activeOpacity={0.8}
|
|
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center', marginTop: 10 }}
|
|
>
|
|
{busy
|
|
? <ActivityIndicator size="small" color="#fff" />
|
|
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_step1_cta')}</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
))}
|
|
</SetupStepCard>
|
|
);
|
|
}
|
|
|
|
function AndroidStep2({
|
|
unlocked,
|
|
done,
|
|
onActivate,
|
|
colors,
|
|
t,
|
|
}: {
|
|
unlocked: boolean;
|
|
done: boolean;
|
|
onActivate: () => Promise<unknown>;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handlePress() {
|
|
if (busy) return;
|
|
setBusy(true);
|
|
try { await onActivate(); } finally { setBusy(false); }
|
|
}
|
|
|
|
return (
|
|
<SetupStepCard
|
|
stepNumber={4}
|
|
title={t('blocker.android_step2_title')}
|
|
subtitle={done ? t('blocker.android_step2_subtitle_done') : t('blocker.android_step2_subtitle_pending')}
|
|
done={done}
|
|
unlocked={unlocked}
|
|
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 3 })}
|
|
colors={colors}
|
|
>
|
|
{unlocked && !done && (
|
|
<View style={{ gap: 8, marginTop: 10 }}>
|
|
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 12, padding: 12, gap: 6 }}>
|
|
{[
|
|
t('blocker.android_step2_instruction1'),
|
|
t('blocker.android_step2_instruction2'),
|
|
t('blocker.android_step2_instruction3'),
|
|
].map((line, i) => (
|
|
<Text
|
|
key={i}
|
|
style={{
|
|
fontSize: 13,
|
|
fontFamily: i === 0 ? 'Nunito_700Bold' : 'Nunito_600SemiBold',
|
|
color: i === 0 ? colors.brandOrange : colors.text,
|
|
lineHeight: 18,
|
|
}}
|
|
>
|
|
{line}
|
|
</Text>
|
|
))}
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={handlePress}
|
|
activeOpacity={0.8}
|
|
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center', marginTop: 2 }}
|
|
>
|
|
{busy
|
|
? <ActivityIndicator size="small" color="#fff" />
|
|
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_step2_cta')}</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 16 }}>
|
|
{t('blocker.android_step2_note')}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</SetupStepCard>
|
|
);
|
|
}
|
|
|
|
function AndroidStepBattery({
|
|
unlocked,
|
|
done,
|
|
onRequest,
|
|
onOpenDetails,
|
|
colors,
|
|
t,
|
|
}: {
|
|
unlocked: boolean;
|
|
done: boolean;
|
|
onRequest: () => Promise<{ opened: boolean; alreadyIgnored?: boolean }>;
|
|
onOpenDetails: () => Promise<{ opened: boolean }>;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handlePress() {
|
|
if (done || busy) return;
|
|
setBusy(true);
|
|
try { await onRequest(); } finally { setBusy(false); }
|
|
}
|
|
|
|
return (
|
|
<SetupStepCard
|
|
stepNumber={3}
|
|
title={t('blocker.android_battery_title')}
|
|
subtitle={done ? t('blocker.android_battery_subtitle_done') : t('blocker.android_battery_subtitle_pending')}
|
|
done={done}
|
|
unlocked={unlocked}
|
|
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 2 })}
|
|
colors={colors}
|
|
>
|
|
{unlocked && !done && (
|
|
<View style={{ gap: 8, marginTop: 10 }}>
|
|
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 12, padding: 12 }}>
|
|
<Text style={{ fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.text, lineHeight: 18 }}>
|
|
{t('blocker.android_battery_body')}
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={handlePress}
|
|
activeOpacity={0.8}
|
|
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center', marginTop: 2 }}
|
|
>
|
|
{busy
|
|
? <ActivityIndicator size="small" color="#fff" />
|
|
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_battery_cta')}</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={() => { void onOpenDetails(); }} activeOpacity={0.7} style={{ paddingVertical: 4, alignItems: 'center' }}>
|
|
<Text style={{ fontSize: 12, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted, textAlign: 'center', lineHeight: 16 }}>
|
|
{t('blocker.android_battery_samsung_hint')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</SetupStepCard>
|
|
);
|
|
}
|
|
|
|
function AndroidStep3({
|
|
unlocked,
|
|
done,
|
|
onRequestAdmin,
|
|
colors,
|
|
t,
|
|
}: {
|
|
unlocked: boolean;
|
|
done: boolean;
|
|
onRequestAdmin: () => Promise<{ launched: boolean }>;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
|
|
}) {
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handlePress() {
|
|
if (done || busy) return;
|
|
setBusy(true);
|
|
try { await onRequestAdmin(); } finally { setBusy(false); }
|
|
}
|
|
|
|
return (
|
|
<SetupStepCard
|
|
stepNumber={2}
|
|
title={t('blocker.android_step3_title')}
|
|
subtitle={done ? t('blocker.android_step3_subtitle_done') : t('blocker.android_step3_subtitle_pending')}
|
|
done={done}
|
|
unlocked={unlocked}
|
|
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 1 })}
|
|
colors={colors}
|
|
>
|
|
{unlocked && !done && (
|
|
<View style={{ gap: 10, marginTop: 10 }}>
|
|
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 10, padding: 10, flexDirection: 'row', gap: 8 }}>
|
|
<Ionicons name="information-circle-outline" size={15} color={colors.textMuted} style={{ marginTop: 1 }} />
|
|
<Text style={{ flex: 1, fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 17 }}>
|
|
{t('blocker.android_step3_warning')}
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={handlePress}
|
|
activeOpacity={0.8}
|
|
style={{ backgroundColor: colors.success, borderRadius: 10, paddingVertical: 10, alignItems: 'center' }}
|
|
>
|
|
{busy
|
|
? <ActivityIndicator size="small" color="#fff" />
|
|
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_step3_cta')}</Text>
|
|
}
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</SetupStepCard>
|
|
);
|
|
}
|
|
|
|
function SetupStepCard({
|
|
stepNumber,
|
|
title,
|
|
subtitle,
|
|
done,
|
|
unlocked,
|
|
lockedHint,
|
|
colors,
|
|
children,
|
|
}: {
|
|
stepNumber: number;
|
|
title: string;
|
|
subtitle: string;
|
|
done: boolean;
|
|
unlocked: boolean;
|
|
lockedHint?: string;
|
|
colors: ReturnType<typeof import('../../lib/theme').useColors>;
|
|
children?: ReactNode;
|
|
}) {
|
|
const borderColor = done ? '#86efac' : unlocked ? colors.border : colors.border;
|
|
const cardBg = done ? '#f0fdf4' : colors.surface;
|
|
const numberBg = done ? '#dcfce7' : unlocked ? colors.surfaceElevated : colors.surfaceElevated;
|
|
const numberColor = done ? colors.success : unlocked ? colors.text : colors.textMuted;
|
|
const titleColor = done ? colors.success : unlocked ? colors.text : colors.textMuted;
|
|
|
|
return (
|
|
<View style={{ backgroundColor: cardBg, borderWidth: 1, borderColor, borderRadius: 16, padding: 14 }}>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
<View style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: numberBg, alignItems: 'center', justifyContent: 'center' }}>
|
|
{done
|
|
? <Ionicons name="checkmark" size={18} color={colors.success} />
|
|
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: numberColor }}>{stepNumber}</Text>
|
|
}
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: titleColor }}>{title}</Text>
|
|
{!!subtitle && (
|
|
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, marginTop: 2, lineHeight: 17 }}>{subtitle}</Text>
|
|
)}
|
|
</View>
|
|
{done && <Ionicons name="checkmark-circle" size={24} color={colors.success} />}
|
|
</View>
|
|
{lockedHint && !done && (
|
|
<View style={{ marginTop: 8, flexDirection: 'row', gap: 6, alignItems: 'center' }}>
|
|
<Ionicons name="lock-closed-outline" size={13} color={colors.textMuted} />
|
|
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>{lockedHint}</Text>
|
|
</View>
|
|
)}
|
|
{children}
|
|
</View>
|
|
);
|
|
}
|
|
|