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>
226 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
}
|