chahinebrini 72cd195d36 feat(blocker): VIP-Domain-Sektion + Cooldown-Friction beim Entfernen
Neue VIP-Sektion in der Blocker-Page: eigene Web-Custom-Domains als Tiles mit
Zaehler (X/10 Pro, X/20 Legend), Hinzufuegen frei. Entfernen geht durch einen
3-Klick-Friction-Flow (RemoveDomainSheet, gespiegelt vom Deactivation-Muster) —
kein Block-Abbau im Craving-Moment. free-Tier-Zweig entfernt (kein Free mehr).

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

226 lines
6.6 KiB
TypeScript

import { View, Text, TouchableOpacity, ScrollView, ActionSheetIOS, Platform, Alert } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme';
import { FormSheet } from '../FormSheet';
type Props = {
visible: boolean;
domain: string;
onClose: () => void;
onConfirmRemove: () => Promise<void>;
};
/**
* 3-Click Friction-Gate vor dem Entfernen einer eigenen Web-Domain.
*
* Selbes UX-Muster wie DeactivationExplainerSheet:
* Click 1: User tippt Papierkorb-Icon → Sheet öffnet sich (dieser Component)
* Click 2: User liest Kontext → primäre Aktion = "Behalten" (Deflect)
* sekundäre Aktion = "Trotzdem entfernen" (klein, destructive)
* Click 3: ActionSheet / Alert zur finalen Bestätigung → removeDomain()
*
* Kein Cooldown wird gestartet — nur der Domain-Delete-Endpoint.
* Die Verzögerung entsteht durch das bewusste 3-Schritt-UX, nicht durch
* eine zeitliche Sperre (anders als beim Schutz-Deaktivieren).
*/
export function RemoveDomainSheet({ visible, domain, onClose, onConfirmRemove }: Props) {
const { t } = useTranslation();
const colors = useColors();
const insets = useSafeAreaInsets();
const [submitting, setSubmitting] = useState(false);
function showFinalConfirm() {
const title = t('blocker.remove_domain_actionsheet_title');
const message = t('blocker.remove_domain_actionsheet_message', { domain });
const cancelLabel = t('common.cancel');
const confirmLabel = t('blocker.remove_domain_confirm_cta');
if (Platform.OS === 'ios') {
ActionSheetIOS.showActionSheetWithOptions(
{
title,
message,
options: [cancelLabel, confirmLabel],
destructiveButtonIndex: 1,
cancelButtonIndex: 0,
},
async (idx) => {
if (idx === 1) await runRemove();
},
);
} else {
Alert.alert(title, message, [
{ text: cancelLabel, style: 'cancel' },
{ text: confirmLabel, style: 'destructive', onPress: runRemove },
]);
}
}
async function runRemove() {
setSubmitting(true);
try {
await onConfirmRemove();
onClose();
} catch (e: any) {
Alert.alert(t('common.error'), e?.message ?? t('blocker.remove_domain_failed'));
} finally {
setSubmitting(false);
}
}
return (
<FormSheet
visible={visible}
onClose={onClose}
title={t('blocker.remove_domain_sheet_heading')}
initialHeightPct={0.58}
minHeightPct={0.3}
safeAreaBottom={false}
>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 20, paddingBottom: Math.max(insets.bottom, 12) + 24, gap: 18 }}
>
<Text style={{ fontSize: 22, fontFamily: 'Nunito_800ExtraBold', color: colors.text }}>
{t('blocker.remove_domain_title')}
</Text>
<View
style={{
backgroundColor: colors.surfaceElevated,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}}
>
<Ionicons name="globe-outline" size={16} color={colors.textMuted} />
<Text
numberOfLines={1}
style={{ fontSize: 14, fontFamily: 'Nunito_600SemiBold', color: colors.text, flex: 1 }}
>
{domain}
</Text>
</View>
<Text
style={{
fontSize: 15,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
lineHeight: 22,
}}
>
{t('blocker.remove_domain_intro')}
</Text>
<View style={{ gap: 12 }}>
<BulletRow
icon="shield-outline"
title={t('blocker.remove_domain_bullet1_title')}
text={t('blocker.remove_domain_bullet1_text')}
/>
<BulletRow
icon="alert-circle-outline"
title={t('blocker.remove_domain_bullet2_title')}
text={t('blocker.remove_domain_bullet2_text')}
/>
</View>
<View style={{ height: 8 }} />
<TouchableOpacity onPress={onClose} activeOpacity={0.85}>
<View
style={{
backgroundColor: colors.success,
borderRadius: 14,
paddingVertical: 16,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
}}
>
<Ionicons name="shield-checkmark" size={18} color="#fff" />
<Text style={{ fontSize: 15, fontFamily: 'Nunito_700Bold', color: '#fff' }}>
{t('blocker.remove_domain_keep_cta')}
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={showFinalConfirm}
disabled={submitting}
hitSlop={8}
activeOpacity={0.5}
style={{
opacity: submitting ? 0.5 : 1,
alignSelf: 'center',
paddingVertical: 12,
}}
>
<Text
style={{
fontSize: 13,
fontFamily: 'Nunito_400Regular',
color: colors.error,
}}
>
{submitting ? t('blocker.remove_domain_removing') : t('blocker.remove_domain_remove_anyway')}
</Text>
</TouchableOpacity>
</ScrollView>
</FormSheet>
);
}
function BulletRow({
icon,
title,
text,
}: {
icon: React.ComponentProps<typeof Ionicons>['name'];
title: string;
text: string;
}) {
const colors = useColors();
return (
<View style={{ flexDirection: 'row', gap: 12 }}>
<View
style={{
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Ionicons name={icon} size={18} color={colors.textMuted} />
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}>
{title}
</Text>
<Text
style={{
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
marginTop: 2,
lineHeight: 17,
}}
>
{text}
</Text>
</View>
</View>
);
}