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
This commit is contained in:
220
client-app/app/index.tsx
Normal file
220
client-app/app/index.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user