Files
dchain/client-app/app/index.tsx
vsecoder 7e7393e4f8 chore: initial commit for v0.0.1
DChain single-node blockchain + React Native messenger client.

Core:
- PBFT consensus with multi-sig validator admission + equivocation slashing
- BadgerDB + schema migration scaffold (CurrentSchemaVersion=0)
- libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1)
- Native Go contracts (username_registry) alongside WASM (wazero)
- WebSocket gateway with topic-based fanout + Ed25519-nonce auth
- Relay mailbox with NaCl envelope encryption (X25519 + Ed25519)
- Prometheus /metrics, per-IP rate limit, body-size cap

Deployment:
- Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus
- 3-node dev compose (docker-compose.yml) with mocked internet topology
- 3-validator prod compose (deploy/prod/) for federation
- Auto-update from Gitea via /api/update-check + systemd timer
- Build-time version injection (ldflags → node --version)
- UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER)

Client (client-app/):
- Expo / React Native / NativeWind
- E2E NaCl encryption, typing indicator, contact requests
- Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch

Documentation:
- README.md, CHANGELOG.md, CONTEXT.md
- deploy/single/README.md with 6 operator scenarios
- deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design
- docs/contracts/*.md per contract
2026-04-17 14:16:44 +03:00

221 lines
7.7 KiB
TypeScript

/**
* Welcome / landing screen.
* - Node URL input with live ping + QR scanner
* - Create / Import account buttons
* Redirects to (app)/chats if key already loaded.
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
View, Text, TextInput, Pressable,
ScrollView, Alert, ActivityIndicator,
} from 'react-native';
import { router } from 'expo-router';
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useStore } from '@/lib/store';
import { saveSettings } from '@/lib/storage';
import { setNodeUrl, getNetStats } from '@/lib/api';
import { Button } from '@/components/ui/Button';
export default function WelcomeScreen() {
const keyFile = useStore(s => s.keyFile);
const settings = useStore(s => s.settings);
const setSettings = useStore(s => s.setSettings);
const [nodeInput, setNodeInput] = useState('');
const [scanning, setScanning] = useState(false);
const [checking, setChecking] = useState(false);
const [nodeOk, setNodeOk] = useState<boolean | null>(null);
const [permission, requestPermission] = useCameraPermissions();
useEffect(() => {
if (keyFile) router.replace('/(app)/chats');
}, [keyFile]);
useEffect(() => {
setNodeInput(settings.nodeUrl);
}, [settings.nodeUrl]);
const applyNode = useCallback(async (url: string) => {
const clean = url.trim().replace(/\/$/, '');
if (!clean) return;
setChecking(true);
setNodeOk(null);
setNodeUrl(clean);
try {
await getNetStats();
setNodeOk(true);
const next = { ...settings, nodeUrl: clean };
setSettings(next);
await saveSettings(next);
} catch {
setNodeOk(false);
} finally {
setChecking(false);
}
}, [settings, setSettings]);
const onQrScanned = useCallback(({ data }: { data: string }) => {
setScanning(false);
let url = data.trim();
try { const p = JSON.parse(url); if (p.nodeUrl) url = p.nodeUrl; } catch {}
setNodeInput(url);
applyNode(url);
}, [applyNode]);
const openScanner = async () => {
if (!permission?.granted) {
const { granted } = await requestPermission();
if (!granted) {
Alert.alert('Camera permission required', 'Allow camera access to scan QR codes.');
return;
}
}
setScanning(true);
};
// ── QR Scanner overlay ───────────────────────────────────────────────────
if (scanning) {
return (
<View style={{ flex: 1, backgroundColor: '#000' }}>
<CameraView
style={{ flex: 1 }}
facing="back"
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
onBarcodeScanned={onQrScanned}
/>
<View style={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
alignItems: 'center', justifyContent: 'center',
}}>
<View style={{
width: 240, height: 240,
borderWidth: 2, borderColor: 'white', borderRadius: 16,
}} />
<Text style={{ color: 'white', marginTop: 20, opacity: 0.8 }}>
Point at a DChain node QR code
</Text>
</View>
<Pressable
onPress={() => setScanning(false)}
style={{
position: 'absolute', top: 56, left: 16,
backgroundColor: 'rgba(0,0,0,0.6)', borderRadius: 20,
paddingHorizontal: 16, paddingVertical: 8,
}}
>
<Text style={{ color: 'white', fontSize: 16 }}> Cancel</Text>
</Pressable>
</View>
);
}
// ── Main screen ──────────────────────────────────────────────────────────
const statusColor = nodeOk === true ? '#3fb950' : nodeOk === false ? '#f85149' : '#8b949e';
return (
<ScrollView
style={{ flex: 1, backgroundColor: '#0d1117' }}
contentContainerStyle={{ flexGrow: 1, paddingHorizontal: 24, paddingTop: 80, paddingBottom: 40 }}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
>
{/* Logo ─ takes remaining space above, centered */}
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}>
<View style={{
width: 96, height: 96, borderRadius: 24,
backgroundColor: '#2563eb', alignItems: 'center', justifyContent: 'center',
}}>
<Text style={{ fontSize: 48 }}></Text>
</View>
<Text style={{ color: '#fff', fontSize: 36, fontWeight: '700', letterSpacing: -1 }}>
DChain
</Text>
<Text style={{ color: '#8b949e', textAlign: 'center', fontSize: 15, lineHeight: 22 }}>
Decentralised E2E-encrypted messenger.{'\n'}Your keys. Your messages.
</Text>
</View>
{/* Bottom section ─ node input + buttons */}
<View style={{ gap: 12, marginTop: 40 }}>
{/* Node URL label */}
<Text style={{ color: '#8b949e', fontSize: 11, fontWeight: '600',
textTransform: 'uppercase', letterSpacing: 1.2, marginBottom: 2 }}>
Node URL
</Text>
{/* Input row */}
<View style={{ flexDirection: 'row', gap: 8 }}>
<View style={{
flex: 1, flexDirection: 'row', alignItems: 'center',
backgroundColor: '#21262d', borderWidth: 1, borderColor: '#30363d',
borderRadius: 12, paddingHorizontal: 12, gap: 8,
}}>
{/* Status dot */}
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: statusColor }} />
<TextInput
value={nodeInput}
onChangeText={t => { setNodeInput(t); setNodeOk(null); }}
onEndEditing={() => applyNode(nodeInput)}
onSubmitEditing={() => applyNode(nodeInput)}
placeholder="http://192.168.1.10:8081"
placeholderTextColor="#8b949e"
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
returnKeyType="done"
style={{ flex: 1, color: '#fff', fontSize: 14, paddingVertical: 14 }}
/>
{checking
? <ActivityIndicator size="small" color="#8b949e" />
: nodeOk === true
? <Text style={{ color: '#3fb950', fontSize: 16 }}></Text>
: nodeOk === false
? <Text style={{ color: '#f85149', fontSize: 14 }}></Text>
: null
}
</View>
{/* QR button */}
<Pressable
onPress={openScanner}
style={({ pressed }) => ({
width: 48, alignItems: 'center', justifyContent: 'center',
backgroundColor: '#21262d', borderWidth: 1, borderColor: '#30363d',
borderRadius: 12, opacity: pressed ? 0.7 : 1,
})}
>
<Text style={{ fontSize: 22 }}></Text>
</Pressable>
</View>
{/* Status text */}
{nodeOk === true && (
<Text style={{ color: '#3fb950', fontSize: 12, marginTop: -4 }}>
Node connected
</Text>
)}
{nodeOk === false && (
<Text style={{ color: '#f85149', fontSize: 12, marginTop: -4 }}>
Cannot reach node check URL and that the node is running
</Text>
)}
{/* Buttons */}
<View style={{ gap: 10, marginTop: 4 }}>
<Button size="lg" onPress={() => router.push('/(auth)/create')}>
Create New Account
</Button>
<Button variant="outline" size="lg" onPress={() => router.push('/(auth)/import')}>
Import Existing Key
</Button>
</View>
</View>
</ScrollView>
);
}