/** * Add New Contact — DChain explorer design style. * Sends CONTACT_REQUEST on-chain with correct amount/fee fields. */ import React, { useState } from 'react'; import { View, Text, ScrollView, Alert, TouchableOpacity, TextInput, } from 'react-native'; import { router } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useStore } from '@/lib/store'; import { getIdentity, buildContactRequestTx, submitTx } from '@/lib/api'; import { shortAddr } from '@/lib/crypto'; import { formatAmount } from '@/lib/utils'; import { Avatar } from '@/components/ui/Avatar'; const C = { bg: '#0b1220', surface: '#111a2b', surface2:'#162035', line: '#1c2840', text: '#e6edf9', muted: '#98a7c2', accent: '#7db5ff', ok: '#41c98a', warn: '#f0b35a', err: '#ff7a87', } as const; const MIN_CONTACT_FEE = 5000; const FEE_OPTIONS = [ { label: '5 000 µT', value: 5_000, note: 'минимум' }, { label: '10 000 µT', value: 10_000, note: 'стандарт' }, { label: '50 000 µT', value: 50_000, note: 'приоритет' }, ]; interface Resolved { address: string; nickname?: string; x25519?: string; } export default function NewContactScreen() { const keyFile = useStore(s => s.keyFile); const settings = useStore(s => s.settings); const balance = useStore(s => s.balance); const insets = useSafeAreaInsets(); const [query, setQuery] = useState(''); const [intro, setIntro] = useState(''); const [fee, setFee] = useState(MIN_CONTACT_FEE); const [resolved, setResolved] = useState(null); const [searching, setSearching] = useState(false); const [sending, setSending] = useState(false); const [error, setError] = useState(null); async function search() { const q = query.trim(); if (!q) return; setSearching(true); setResolved(null); setError(null); try { let address = q; // @username lookup via registry contract if (q.startsWith('@') || (!q.match(/^[0-9a-f]{64}$/i) && !q.startsWith('DC'))) { const name = q.replace('@', ''); const { resolveUsername } = await import('@/lib/api'); const addr = await resolveUsername(settings.contractId, name); if (!addr) { setError(`@${name} не зарегистрирован в сети.`); return; } address = addr; } // Fetch identity to get nickname and x25519 key const identity = await getIdentity(address); setResolved({ address: identity?.pub_key ?? address, nickname: identity?.nickname || undefined, x25519: identity?.x25519_pub || undefined, }); } catch (e: any) { setError(e.message); } finally { setSearching(false); } } async function sendRequest() { if (!resolved || !keyFile) return; if (balance < fee + 1000) { Alert.alert('Недостаточно средств', `Нужно ${formatAmount(fee + 1000)} (fee + комиссия сети).`); return; } setSending(true); setError(null); try { const tx = buildContactRequestTx({ from: keyFile.pub_key, to: resolved.address, contactFee: fee, intro: intro.trim() || undefined, privKey: keyFile.priv_key, }); await submitTx(tx); Alert.alert( 'Запрос отправлен', `Контакту ${resolved.nickname ? '@' + resolved.nickname : shortAddr(resolved.address)} отправлен запрос.`, [{ text: 'OK', onPress: () => router.back() }], ); } catch (e: any) { setError(e.message); } finally { setSending(false); } } const displayName = resolved ? (resolved.nickname ? `@${resolved.nickname}` : shortAddr(resolved.address)) : null; return ( {/* Header */} router.back()} activeOpacity={0.6} style={{ padding: 8 }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > Добавить контакт {/* Search */} Адрес или @username { setQuery(t); setError(null); }} onSubmitEditing={search} placeholder="@username или 64-символьный hex" placeholderTextColor={C.muted} autoCapitalize="none" autoCorrect={false} style={{ color: C.text, fontSize: 14, height: 44 }} /> {searching ? '…' : 'Найти'} {/* Error */} {error && ( ⚠ {error} )} {/* Resolved contact card */} {resolved && ( {resolved.nickname && ( @{resolved.nickname} )} {resolved.address} {resolved.x25519 && ( ✓ Зашифрованные сообщения поддерживаются )} Найден )} {/* Intro + fee (shown after search succeeds) */} {resolved && ( <> {/* Intro */} Сообщение (необязательно) {intro.length}/280 {/* Fee selector */} Антиспам-сбор (отправляется контакту) {FEE_OPTIONS.map(opt => ( setFee(opt.value)} style={{ flex: 1, borderRadius: 10, paddingVertical: 10, alignItems: 'center', backgroundColor: fee === opt.value ? 'rgba(125,181,255,0.15)' : C.surface, borderWidth: fee === opt.value ? 1 : 0, borderColor: fee === opt.value ? C.accent : 'transparent', }} > {opt.label} {opt.note} ))} {/* Balance info */} Ваш баланс: = fee + 1000 ? C.text : C.err, fontSize: 13, fontWeight: '600' }}> {formatAmount(balance)} Итого: {formatAmount(fee + 1000)} {/* Send button */} {sending ? 'Отправка…' : 'Отправить запрос'} )} {/* Hint when no search yet */} {!resolved && !error && ( Введите @username или вставьте 64-символьный hex-адрес пользователя DChain. )} ); }