chahinebrini 80d89303f5 fix(native/blocker): pass kind to addDomain so mail patterns route correctly
User added info@info.mail-slotoro.com and it landed in Eigene Domains
as type=web instead of in Eigene Mails as type=mail_domain. Bug trace:

1. AddDomainSheet detects kind='mail' from the @ in the user's input
2. mailDomain() strips the local-part → "info.mail-slotoro.com"
3. handleAdd calls onAdd(pattern) — only the stripped string, no kind
4. useCustomDomains.addDomain then sends { pattern } with no kind
5. Backend Variante C auto-detect keys on @ in the pattern — but the
   pattern no longer contains @ (frontend already stripped it), so the
   detector falls into the kind='web' branch

Fix: pass the kind explicitly from the sheet through the prop chain.
AddDomainSheet.onAdd is now (pattern, kind?) — the sheet's handleAdd
forwards the kind it detected. blocker.tsx's onAdd handler threads
it into addDomain so the body includes { pattern, kind }. Backend
then takes the explicit path and stores type='mail_domain' for the
already-stripped value. Auto-detect on bare pattern (no kind) still
works for any caller that genuinely doesn't know — that path just
isn't used by the sheet anymore.
2026-05-16 03:06:34 +02:00

393 lines
12 KiB
TypeScript

import { useState } from 'react';
import {
ActivityIndicator,
Image,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import {
isValidDomain,
normalizeDomain,
type Tier,
} from '../../hooks/useCustomDomains';
import { useColors, type ColorScheme } from '../../lib/theme';
import { FormSheet } from '../FormSheet';
type Props = {
visible: boolean;
tier: Tier;
onClose: () => void;
onAdd: (pattern: string, kind?: 'web' | 'mail') => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>;
};
function detectKind(input: string): 'web' | 'mail' | null {
const raw = input.trim();
if (!raw) return null;
if (raw.includes('@')) return 'mail';
if (raw.includes('.')) return 'web';
return null;
}
function mailDomain(input: string): string {
const raw = input.trim();
const atIdx = raw.lastIndexOf('@');
if (atIdx === -1) return raw.toLowerCase();
return raw.slice(atIdx + 1).trim().toLowerCase();
}
export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
const { t } = useTranslation();
const colors = useColors();
const [input, setInput] = useState('');
const [confirmPermanent, setConfirmPermanent] = useState(false);
const [adding, setAdding] = useState(false);
const [error, setError] = useState<string | null>(null);
const kind = detectKind(input);
const normalizedWeb = kind === 'web' ? normalizeDomain(input) : '';
const normalizedMail = kind === 'mail' ? mailDomain(input) : '';
function close() {
setInput('');
setConfirmPermanent(false);
setError(null);
onClose();
}
function isInputValid(): boolean {
if (kind === 'web') return isValidDomain(input);
if (kind === 'mail') return normalizedMail.length > 0;
return false;
}
async function handleAdd() {
if (!isInputValid() || !confirmPermanent || adding) return;
setAdding(true);
setError(null);
const pattern = kind === 'web' ? normalizeDomain(input) : normalizedMail;
// Pass kind explicitly — we've already stripped the local-part for mail,
// so the backend's auto-detect (which keys on the "@" character) can no
// longer infer the type from the pattern alone. Without this hint a
// "info@only4-subscribers.com" entry would land as type=web because
// the @ disappeared during the strip.
const result = await onAdd(pattern, kind === 'mail' ? 'mail' : 'web');
setAdding(false);
if (result.ok) {
close();
return;
}
if (result.alreadyGlobal) {
setError(t('blocker.add_sheet_already_global', { domain: pattern }));
} else if (result.error?.includes('WEB_LIMIT_REACHED')) {
setError(t('blocker.error_web_limit_reached'));
} else if (result.error?.includes('MAIL_LIMIT_REACHED')) {
setError(t('blocker.error_mail_limit_reached'));
} else {
setError(result.error ?? t('blocker.add_sheet_add_failed'));
}
}
const warningText =
tier.plan === 'free'
? t('blocker.add_sheet_warning_free')
: t('blocker.add_sheet_warning_pro');
const canSubmit = isInputValid() && confirmPermanent && !adding;
return (
<FormSheet
visible={visible}
onClose={close}
title={t('blocker.add_sheet_title')}
initialHeightPct={0.78}
growWithKeyboard
>
<ScrollView
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
contentContainerStyle={{ padding: 16, gap: 12 }}
>
{/* Input field */}
<View style={{ gap: 6 }}>
<Text style={{ fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.text }}>
{t('blocker.add_sheet_label')}
</Text>
<TextInput
value={input}
onChangeText={(v) => { setInput(v); setError(null); }}
placeholder={t('blocker.add_sheet_placeholder')}
placeholderTextColor={colors.textMuted}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
style={{
backgroundColor: colors.surfaceElevated,
borderRadius: 10,
padding: 12,
fontSize: 14,
fontFamily: 'Nunito_400Regular',
color: colors.text,
borderWidth: 1,
borderColor: error ? '#dc2626' : colors.border,
}}
/>
{error && (
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: '#dc2626' }}>
{error}
</Text>
)}
</View>
{/* Help text */}
<View
style={{
flexDirection: 'row',
gap: 8,
padding: 12,
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
}}
>
<Ionicons
name="information-circle-outline"
size={16}
color={colors.textMuted}
style={{ marginTop: 1 }}
/>
<Text
style={{
flex: 1,
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
lineHeight: 17,
}}
>
{t('blocker.add_sheet_help')}
</Text>
</View>
{/* Preview card */}
<PreviewCard
kind={kind}
normalizedWeb={normalizedWeb}
normalizedMail={normalizedMail}
placeholder={t('blocker.add_sheet_placeholder')}
colors={colors}
t={t}
/>
{/* Warning card */}
<View
style={{
flexDirection: 'row',
gap: 10,
padding: 12,
backgroundColor: '#fef3c7',
borderRadius: 12,
borderWidth: 1,
borderColor: '#fcd34d',
}}
>
<Ionicons name="lock-closed" size={18} color="#92400e" style={{ marginTop: 1 }} />
<Text
style={{
flex: 1,
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: '#92400e',
lineHeight: 17,
}}
>
{warningText}
</Text>
</View>
{/* Confirm checkbox */}
<TouchableOpacity
onPress={() => setConfirmPermanent((v) => !v)}
activeOpacity={0.7}
style={{
flexDirection: 'row',
alignItems: 'flex-start',
gap: 10,
paddingVertical: 4,
}}
>
<View
style={{
width: 22,
height: 22,
borderRadius: 6,
borderWidth: 1.5,
borderColor: confirmPermanent ? colors.success : colors.border,
backgroundColor: confirmPermanent ? colors.success : colors.bg,
alignItems: 'center',
justifyContent: 'center',
marginTop: 1,
}}
>
{confirmPermanent && <Ionicons name="checkmark" size={16} color="#fff" />}
</View>
<Text
style={{
flex: 1,
fontSize: 13,
fontFamily: 'Nunito_400Regular',
color: colors.text,
lineHeight: 18,
}}
>
{t('blocker.add_sheet_confirm_permanent')}
</Text>
</TouchableOpacity>
{/* Buttons */}
<View style={{ flexDirection: 'row', gap: 10, marginTop: 4 }}>
<TouchableOpacity onPress={close} activeOpacity={0.8} style={{ flex: 1 }}>
<View
style={{
borderRadius: 14,
paddingVertical: 14,
alignItems: 'center',
backgroundColor: colors.surfaceElevated,
borderWidth: 1,
borderColor: colors.border,
}}
>
<Text style={{ fontSize: 15, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted }}>
{t('common.cancel')}
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={handleAdd}
disabled={!canSubmit}
activeOpacity={0.85}
style={{ flex: 2 }}
>
<View
style={{
backgroundColor: canSubmit ? '#dc2626' : '#d4d4d4',
borderRadius: 14,
paddingVertical: 14,
alignItems: 'center',
}}
>
{adding ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={{ fontSize: 15, fontFamily: 'Nunito_700Bold', color: '#fff' }}>
{t('blocker.add_sheet_cta')}
</Text>
)}
</View>
</TouchableOpacity>
</View>
</ScrollView>
</FormSheet>
);
}
// ─── PreviewCard ──────────────────────────────────────────────────────────────
function PreviewCard({
kind,
normalizedWeb,
normalizedMail,
placeholder,
colors,
t,
}: {
kind: 'web' | 'mail' | null;
normalizedWeb: string;
normalizedMail: string;
placeholder: string;
colors: ColorScheme;
t: (key: string, opts?: Record<string, unknown>) => string;
}) {
if (kind === 'web') {
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
padding: 12,
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
}}
>
<Image
source={{ uri: `https://www.google.com/s2/favicons?domain=${normalizedWeb || 'example.com'}&sz=64` }}
style={{ width: 24, height: 24, borderRadius: 4 }}
/>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('blocker.preview_web', { value: normalizedWeb || '…' })}
</Text>
</View>
<Ionicons name="globe-outline" size={16} color={colors.textMuted} />
</View>
);
}
if (kind === 'mail') {
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
padding: 12,
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
}}
>
<View
style={{
width: 24,
height: 24,
borderRadius: 4,
backgroundColor: '#dbeafe',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Ionicons name="mail-outline" size={14} color="#2563eb" />
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('blocker.preview_mail', { value: normalizedMail || '…' })}
</Text>
</View>
<Ionicons name="mail-outline" size={16} color={colors.textMuted} />
</View>
);
}
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
padding: 12,
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
}}
>
<Ionicons name="warning-outline" size={20} color="#dc2626" />
<Text style={{ flex: 1, fontSize: 11, fontFamily: 'Nunito_400Regular', color: '#dc2626' }}>
{t('blocker.preview_invalid')}
</Text>
</View>
);
}