1. GO_BACK warning & stuck screens
When a deep link or direct push put the user on /feed/[id],
/profile/[address], /compose, or /settings without any prior stack
entry, tapping the header chevron emitted:
"ERROR The action 'GO_BACK' was not handled by any navigator"
and did nothing — user was stuck.
New helper lib/utils.safeBack(fallback = '/(app)/chats') wraps
router.canGoBack() — when there's history it pops; otherwise it
replace-navigates to a sensible fallback (chats list by default,
'/' for auth screens so we land back at the onboarding).
Applied to every header chevron and back-from-detail flow:
app/(app)/chats/[id], app/(app)/compose, app/(app)/feed/[id]
(header + onDeleted), app/(app)/feed/tag/[tag],
app/(app)/profile/[address], app/(app)/new-contact (header + OK
button on request-sent alert), app/(app)/settings,
app/(auth)/create, app/(auth)/import.
2. Prevent self-contact-request
new-contact.tsx now compares the resolved address against
keyFile.pub_key at two points:
- right after resolveUsername + getIdentity in search() — before
the profile card even renders, so the user doesn't see the
"Send request" CTA for themselves.
- inside sendRequest() as a belt-and-braces guard in case the
check was somehow bypassed.
The search path shows an inline error ("That's you. You can't
send a contact request to yourself."); sendRequest falls back to
an Alert with the same meaning. Both compare case-insensitively
against the pubkey hex so mixed-case pastes work.
Technically the server would still accept a self-request (the
chain stores it under contact_in:<self>:<self>), but it's a dead-
end UX-wise — the user can't chat with themselves — so the client
blocks it preemptively instead of letting users pay the fee for
nothing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
4.8 KiB
TypeScript
141 lines
4.8 KiB
TypeScript
/**
|
|
* Create Account — dark minimalist.
|
|
* Генерирует Ed25519 + X25519 keypair локально, сохраняет в SecureStore.
|
|
*/
|
|
import React, { useState } from 'react';
|
|
import { View, Text, ScrollView, Alert, Pressable, ActivityIndicator } from 'react-native';
|
|
import { router } from 'expo-router';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { generateKeyFile } from '@/lib/crypto';
|
|
import { saveKeyFile } from '@/lib/storage';
|
|
import { useStore } from '@/lib/store';
|
|
import { safeBack } from '@/lib/utils';
|
|
|
|
import { Header } from '@/components/Header';
|
|
import { IconButton } from '@/components/IconButton';
|
|
|
|
export default function CreateAccountScreen() {
|
|
const insets = useSafeAreaInsets();
|
|
const setKeyFile = useStore(s => s.setKeyFile);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
async function handleCreate() {
|
|
setLoading(true);
|
|
try {
|
|
const kf = generateKeyFile();
|
|
await saveKeyFile(kf);
|
|
setKeyFile(kf);
|
|
router.replace('/(auth)/created' as never);
|
|
} catch (e: any) {
|
|
Alert.alert('Error', e?.message ?? 'Unknown error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
|
|
<Header
|
|
title="Create account"
|
|
left={<IconButton icon="chevron-back" size={36} onPress={() => safeBack('/')} />}
|
|
/>
|
|
<ScrollView contentContainerStyle={{ padding: 14, paddingBottom: 40 }}>
|
|
<Text style={{ color: '#ffffff', fontSize: 17, fontWeight: '700', marginBottom: 4 }}>
|
|
A new identity is created locally
|
|
</Text>
|
|
<Text style={{ color: '#8b8b8b', fontSize: 13, lineHeight: 19, marginBottom: 18 }}>
|
|
Your private key never leaves this device. The app encrypts it in the
|
|
platform secure store.
|
|
</Text>
|
|
|
|
<View
|
|
style={{
|
|
borderRadius: 14,
|
|
backgroundColor: '#0a0a0a',
|
|
borderWidth: 1, borderColor: '#1f1f1f',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<InfoRow icon="key-outline" label="Ed25519 signing key" desc="Your on-chain address and tx signer" first />
|
|
<InfoRow icon="lock-closed-outline" label="X25519 encryption key" desc="End-to-end encryption for messages" />
|
|
<InfoRow icon="phone-portrait-outline" label="Stored on device" desc="Encrypted in SecureStore / Keystore" />
|
|
</View>
|
|
|
|
<View
|
|
style={{
|
|
marginTop: 16,
|
|
padding: 12,
|
|
borderRadius: 12,
|
|
backgroundColor: 'rgba(240,179,90,0.08)',
|
|
borderWidth: 1, borderColor: 'rgba(240,179,90,0.25)',
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4 }}>
|
|
<Ionicons name="warning-outline" size={14} color="#f0b35a" style={{ marginRight: 6 }} />
|
|
<Text style={{ color: '#f0b35a', fontSize: 13, fontWeight: '700' }}>Important</Text>
|
|
</View>
|
|
<Text style={{ color: '#d0a26a', fontSize: 12, lineHeight: 17 }}>
|
|
Export and backup your key file right after creation. If you lose
|
|
it there is no recovery — blockchain has no password reset.
|
|
</Text>
|
|
</View>
|
|
|
|
<Pressable
|
|
onPress={handleCreate}
|
|
disabled={loading}
|
|
style={({ pressed }) => ({
|
|
alignItems: 'center', justifyContent: 'center',
|
|
paddingVertical: 13, borderRadius: 999, marginTop: 20,
|
|
backgroundColor: loading ? '#1a1a1a' : pressed ? '#1a8cd8' : '#1d9bf0',
|
|
})}
|
|
>
|
|
{loading ? (
|
|
<ActivityIndicator color="#ffffff" size="small" />
|
|
) : (
|
|
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 15 }}>
|
|
Generate keys & continue
|
|
</Text>
|
|
)}
|
|
</Pressable>
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function InfoRow({
|
|
icon, label, desc, first,
|
|
}: {
|
|
icon: React.ComponentProps<typeof Ionicons>['name'];
|
|
label: string;
|
|
desc: string;
|
|
first?: boolean;
|
|
}) {
|
|
return (
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
padding: 14,
|
|
gap: 12,
|
|
borderTopWidth: first ? 0 : 1,
|
|
borderTopColor: '#1f1f1f',
|
|
}}
|
|
>
|
|
<View
|
|
style={{
|
|
width: 32, height: 32, borderRadius: 8,
|
|
backgroundColor: '#111111',
|
|
alignItems: 'center', justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Ionicons name={icon} size={16} color="#ffffff" />
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={{ color: '#ffffff', fontSize: 14, fontWeight: '600' }}>{label}</Text>
|
|
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 2 }}>{desc}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|