/** * Import Existing Key screen. * Two methods: * 1. Paste JSON directly into a text field * 2. Pick key.json file via document picker */ import React, { useState } from 'react'; import { View, Text, ScrollView, TextInput, TouchableOpacity, Alert, Pressable, } from 'react-native'; import { router } from 'expo-router'; import * as DocumentPicker from 'expo-document-picker'; import * as Clipboard from 'expo-clipboard'; import { saveKeyFile } from '@/lib/storage'; import { useStore } from '@/lib/store'; import { Button } from '@/components/ui/Button'; import type { KeyFile } from '@/lib/types'; type Tab = 'paste' | 'file'; const REQUIRED_FIELDS: (keyof KeyFile)[] = ['pub_key', 'priv_key', 'x25519_pub', 'x25519_priv']; function validateKeyFile(raw: string): KeyFile { let parsed: any; try { parsed = JSON.parse(raw.trim()); } catch { throw new Error('Invalid JSON — check that you copied the full key file contents.'); } for (const field of REQUIRED_FIELDS) { if (!parsed[field] || typeof parsed[field] !== 'string') { throw new Error(`Missing or invalid field: "${field}"`); } if (!/^[0-9a-f]+$/i.test(parsed[field])) { throw new Error(`Field "${field}" must be a hex string.`); } } return parsed as KeyFile; } export default function ImportKeyScreen() { const setKeyFile = useStore(s => s.setKeyFile); const [tab, setTab] = useState('paste'); const [jsonText, setJsonText] = useState(''); const [fileName, setFileName] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // ── Shared: save validated key and navigate ────────────────────────────── async function applyKey(kf: KeyFile) { setLoading(true); setError(null); try { await saveKeyFile(kf); setKeyFile(kf); router.replace('/(app)/chats'); } catch (e: any) { setError(e.message); } finally { setLoading(false); } } // ── Method 1: paste JSON ───────────────────────────────────────────────── async function handlePasteImport() { setError(null); const text = jsonText.trim(); if (!text) { // Try reading clipboard if field is empty const clip = await Clipboard.getStringAsync(); if (clip) setJsonText(clip); return; } try { const kf = validateKeyFile(text); await applyKey(kf); } catch (e: any) { setError(e.message); } } // ── Method 2: pick file ────────────────────────────────────────────────── async function pickFile() { setError(null); try { const result = await DocumentPicker.getDocumentAsync({ type: ['application/json', 'text/plain', '*/*'], copyToCacheDirectory: true, }); if (result.canceled) return; const asset = result.assets[0]; setFileName(asset.name); // Use fetch() — readAsStringAsync is deprecated in newer expo-file-system const response = await fetch(asset.uri); const raw = await response.text(); const kf = validateKeyFile(raw); await applyKey(kf); } catch (e: any) { setError(e.message); } } const tabStyle = (t: Tab) => ({ flex: 1 as const, paddingVertical: 10, alignItems: 'center' as const, borderBottomWidth: 2, borderBottomColor: tab === t ? '#2563eb' : 'transparent', }); const tabTextStyle = (t: Tab) => ({ fontSize: 14, fontWeight: '600' as const, color: tab === t ? '#fff' : '#8b949e', }); return ( {/* Back */} router.back()} style={{ marginBottom: 24, alignSelf: 'flex-start' }}> ← Back Import Key Restore your account from an existing{' '} key.json. {/* Tabs */} setTab('paste')}> 📋 Paste JSON setTab('file')}> 📁 Open File {/* ── Paste tab ── */} {tab === 'paste' && ( Key JSON { setJsonText(t); setError(null); }} placeholder={'{\n "pub_key": "...",\n "priv_key": "...",\n "x25519_pub": "...",\n "x25519_priv": "..."\n}'} placeholderTextColor="#8b949e" multiline numberOfLines={8} autoCapitalize="none" autoCorrect={false} style={{ color: '#fff', fontFamily: 'monospace', fontSize: 12, lineHeight: 18, minHeight: 160, textAlignVertical: 'top', }} /> {/* Paste from clipboard shortcut */} {!jsonText && ( { const clip = await Clipboard.getStringAsync(); if (clip) { setJsonText(clip); setError(null); } else Alert.alert('Clipboard empty', 'Copy your key JSON first.'); }} style={{ flexDirection: 'row', alignItems: 'center', gap: 8, padding: 12, backgroundColor: '#161b22', borderWidth: 1, borderColor: '#30363d', borderRadius: 12, }} > 📋 Paste from clipboard )} {error && ( ⚠ {error} )} )} {/* ── File tab ── */} {tab === 'file' && ( 📂 {fileName ?? 'Choose key.json'} Tap to browse files {fileName && ( 📄 {fileName} )} {error && ( ⚠ {error} )} {loading && ( Validating key… )} )} {/* Format hint */} Expected format {`{\n "pub_key": "<64 hex chars>",\n "priv_key": "<128 hex chars>",\n "x25519_pub": "<64 hex chars>",\n "x25519_priv": "<64 hex chars>"\n}`} ); }