From e62b72b5bead4c5106324c4f6ef083d66d00b084 Mon Sep 17 00:00:00 2001 From: vsecoder Date: Sat, 18 Apr 2026 23:45:19 +0300 Subject: [PATCH] fix(client): safeBack helper + prevent self-contact-request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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::), 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) --- client-app/app/(app)/chats/[id].tsx | 4 ++-- client-app/app/(app)/compose.tsx | 3 ++- client-app/app/(app)/feed/[id].tsx | 5 +++-- client-app/app/(app)/feed/tag/[tag].tsx | 3 ++- client-app/app/(app)/new-contact.tsx | 23 ++++++++++++++++++---- client-app/app/(app)/profile/[address].tsx | 3 ++- client-app/app/(app)/settings.tsx | 4 ++-- client-app/app/(auth)/create.tsx | 3 ++- client-app/app/(auth)/import.tsx | 3 ++- client-app/lib/utils.ts | 18 +++++++++++++++++ 10 files changed, 54 insertions(+), 15 deletions(-) diff --git a/client-app/app/(app)/chats/[id].tsx b/client-app/app/(app)/chats/[id].tsx index c8fec3b..518cd48 100644 --- a/client-app/app/(app)/chats/[id].tsx +++ b/client-app/app/(app)/chats/[id].tsx @@ -26,7 +26,7 @@ import { encryptMessage } from '@/lib/crypto'; import { sendEnvelope } from '@/lib/api'; import { getWSClient } from '@/lib/ws'; import { appendMessage, loadMessages } from '@/lib/storage'; -import { randomId } from '@/lib/utils'; +import { randomId, safeBack } from '@/lib/utils'; import type { Message } from '@/lib/types'; import { Avatar } from '@/components/Avatar'; @@ -404,7 +404,7 @@ export default function ChatScreen() { ) : (
router.back()} />} + left={ safeBack()} />} title={ - router.back()} hitSlop={8}> + safeBack()} hitSlop={8}> diff --git a/client-app/app/(app)/feed/[id].tsx b/client-app/app/(app)/feed/[id].tsx index 46fb1af..3ab9f59 100644 --- a/client-app/app/(app)/feed/[id].tsx +++ b/client-app/app/(app)/feed/[id].tsx @@ -31,6 +31,7 @@ import { fetchPost, fetchStats, bumpView, formatCount, formatFee, type FeedPostItem, type PostStats, } from '@/lib/feed'; +import { safeBack } from '@/lib/utils'; export default function PostDetailScreen() { const insets = useSafeAreaInsets(); @@ -71,14 +72,14 @@ export default function PostDetailScreen() { const onDeleted = useCallback(() => { // Go back to feed — the post is gone. - router.back(); + safeBack(); }, []); return (
router.back()} />} + left={ safeBack()} />} title="Post" /> diff --git a/client-app/app/(app)/feed/tag/[tag].tsx b/client-app/app/(app)/feed/tag/[tag].tsx index fb8b42a..8e9bc6b 100644 --- a/client-app/app/(app)/feed/tag/[tag].tsx +++ b/client-app/app/(app)/feed/tag/[tag].tsx @@ -17,6 +17,7 @@ import { IconButton } from '@/components/IconButton'; import { PostCard, PostSeparator } from '@/components/feed/PostCard'; import { useStore } from '@/lib/store'; import { fetchHashtag, fetchStats, type FeedPostItem } from '@/lib/feed'; +import { safeBack } from '@/lib/utils'; export default function HashtagScreen() { const insets = useSafeAreaInsets(); @@ -80,7 +81,7 @@ export default function HashtagScreen() {
router.back()} />} + left={ safeBack()} />} title={`#${tag}`} /> diff --git a/client-app/app/(app)/new-contact.tsx b/client-app/app/(app)/new-contact.tsx index ca73657..d2b88f4 100644 --- a/client-app/app/(app)/new-contact.tsx +++ b/client-app/app/(app)/new-contact.tsx @@ -18,7 +18,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useStore } from '@/lib/store'; import { getIdentity, buildContactRequestTx, submitTx, resolveUsername, humanizeTxError } from '@/lib/api'; import { shortAddr } from '@/lib/crypto'; -import { formatAmount } from '@/lib/utils'; +import { formatAmount, safeBack } from '@/lib/utils'; import { Avatar } from '@/components/Avatar'; import { Header } from '@/components/Header'; @@ -64,9 +64,20 @@ export default function NewContactScreen() { if (!addr) { setError(`@${name} is not registered on this chain`); return; } address = addr; } + // Block self-lookup — can't message yourself, and the on-chain + // CONTACT_REQUEST tx would go through but serve no purpose. + if (keyFile && address.toLowerCase() === keyFile.pub_key.toLowerCase()) { + setError("That's you. You can't send a contact request to yourself."); + return; + } const identity = await getIdentity(address); + const resolvedAddr = identity?.pub_key ?? address; + if (keyFile && resolvedAddr.toLowerCase() === keyFile.pub_key.toLowerCase()) { + setError("That's you. You can't send a contact request to yourself."); + return; + } setResolved({ - address: identity?.pub_key ?? address, + address: resolvedAddr, nickname: identity?.nickname || undefined, x25519: identity?.x25519_pub || undefined, }); @@ -79,6 +90,10 @@ export default function NewContactScreen() { async function sendRequest() { if (!resolved || !keyFile) return; + if (resolved.address.toLowerCase() === keyFile.pub_key.toLowerCase()) { + Alert.alert('Can\'t message yourself', "This is your own address."); + return; + } if (balance < fee + 1000) { Alert.alert('Insufficient balance', `Need ${formatAmount(fee + 1000)} (request fee + network).`); return; @@ -96,7 +111,7 @@ export default function NewContactScreen() { Alert.alert( 'Request sent', `A contact request has been sent to ${resolved.nickname ? '@' + resolved.nickname : shortAddr(resolved.address)}.`, - [{ text: 'OK', onPress: () => router.back() }], + [{ text: 'OK', onPress: () => safeBack() }], ); } catch (e: any) { setError(humanizeTxError(e)); @@ -114,7 +129,7 @@ export default function NewContactScreen() {
router.back()} />} + left={ safeBack()} />} /> router.back()} />} + left={ safeBack()} />} /> diff --git a/client-app/app/(app)/settings.tsx b/client-app/app/(app)/settings.tsx index 854fb2d..8414a9c 100644 --- a/client-app/app/(app)/settings.tsx +++ b/client-app/app/(app)/settings.tsx @@ -32,7 +32,7 @@ import { humanizeTxError, } from '@/lib/api'; import { shortAddr } from '@/lib/crypto'; -import { formatAmount } from '@/lib/utils'; +import { formatAmount, safeBack } from '@/lib/utils'; import { Avatar } from '@/components/Avatar'; import { Header } from '@/components/Header'; @@ -335,7 +335,7 @@ export default function SettingsScreen() {
router.back()} />} + left={ safeBack()} />} />
router.back()} />} + left={ safeBack('/')} />} /> diff --git a/client-app/app/(auth)/import.tsx b/client-app/app/(auth)/import.tsx index b2208eb..3456b69 100644 --- a/client-app/app/(auth)/import.tsx +++ b/client-app/app/(auth)/import.tsx @@ -15,6 +15,7 @@ import * as DocumentPicker from 'expo-document-picker'; import * as Clipboard from 'expo-clipboard'; import { saveKeyFile } from '@/lib/storage'; import { useStore } from '@/lib/store'; +import { safeBack } from '@/lib/utils'; import type { KeyFile } from '@/lib/types'; import { Header } from '@/components/Header'; @@ -96,7 +97,7 @@ export default function ImportKeyScreen() {
router.back()} />} + left={ safeBack('/')} />} />