From 8f98969533ad80c090171806ec58a756e526b401 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 14 May 2026 04:25:21 -0800 Subject: [PATCH 1/8] chore(meta): bump expo.version to 1.0.2 --- mobile_app/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile_app/app.json b/mobile_app/app.json index 77020462..32110b2d 100644 --- a/mobile_app/app.json +++ b/mobile_app/app.json @@ -2,7 +2,7 @@ "expo": { "name": "anonmesh", "slug": "anonmesh", - "version": "1.0.1", + "version": "1.0.2", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "anonmesh", From 07621ecd1bc4b959cbd4352451c3d354a043701e Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 14 May 2026 04:26:33 -0800 Subject: [PATCH 2/8] chore(settings): source app version from expo-constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the hardcoded "0.4.1 · build 2026.04" string in favor of Constants.expoConfig?.version so app.json stays the single source of truth. Also bumps package.json version to match app.json (1.0.2). --- mobile_app/package.json | 2 +- mobile_app/screens/SettingsScreen.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mobile_app/package.json b/mobile_app/package.json index 7fb58c0c..708cbdc8 100644 --- a/mobile_app/package.json +++ b/mobile_app/package.json @@ -1,7 +1,7 @@ { "name": "mobile_app", "main": "expo-router/entry", - "version": "1.0.0", + "version": "1.0.2", "scripts": { "postinstall": "node ./scripts/fix-lxmf-gradle.mjs", "start": "expo start", diff --git a/mobile_app/screens/SettingsScreen.tsx b/mobile_app/screens/SettingsScreen.tsx index 8ee77516..4e44d969 100644 --- a/mobile_app/screens/SettingsScreen.tsx +++ b/mobile_app/screens/SettingsScreen.tsx @@ -10,6 +10,7 @@ import { useLxmfContext } from '@/context/LxmfContext'; import { type Href, useRouter } from 'expo-router'; import * as Clipboard from 'expo-clipboard'; import * as Haptics from 'expo-haptics'; +import Constants from 'expo-constants'; import { useNotificationEnabled } from '@/hooks/useNotificationEnabled'; import { useBiometricEnabled } from '@/hooks/useBiometricEnabled'; import { SolanaIcon } from '@/components/onboarding/SolanaIcon'; @@ -250,7 +251,7 @@ export default function SettingsScreen() { about - 0.4.1 · build 2026.04} last /> + {Constants.expoConfig?.version ?? '—'}} last /> Date: Thu, 14 May 2026 04:27:40 -0800 Subject: [PATCH 3/8] feat(primitives): add PreviewBadge component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canonical amber PREVIEW pill for roadmap placeholders, matching the shape pendingCosigns + BeaconRegistry CTAs already use. No consumers in this commit — consumer migrations land in a follow-up cluster so each migration is reviewable on its own. --- .../components/primitives/PreviewBadge.tsx | 38 +++++++++++++++++++ mobile_app/components/primitives/index.ts | 2 + 2 files changed, 40 insertions(+) create mode 100644 mobile_app/components/primitives/PreviewBadge.tsx diff --git a/mobile_app/components/primitives/PreviewBadge.tsx b/mobile_app/components/primitives/PreviewBadge.tsx new file mode 100644 index 00000000..a852e077 --- /dev/null +++ b/mobile_app/components/primitives/PreviewBadge.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { StyleProp, StyleSheet, View, ViewStyle } from "react-native"; + +import { Pill } from "./Pill"; + +interface PreviewBadgeProps { + /** Tail copy appended after "PREVIEW · ". Defaults to "coming soon". */ + label?: string; + /** Alignment inside the wrapper. Defaults to "flex-start". */ + align?: "flex-start" | "center" | "flex-end"; + style?: StyleProp; +} + +/** + * Canonical PREVIEW badge for roadmap placeholders. Render above any placeholder + * UI to mark it as forward-looking (paired with on the CTAs + * inside that placeholder). + * + * Visual shape matches the existing pendingCosigns + BeaconRegistry hero CTA + * pattern. + */ +export function PreviewBadge({ + label = "coming soon", + align = "flex-start", + style, +}: PreviewBadgeProps) { + return ( + + + + ); +} + +const styles = StyleSheet.create({ + wrapper: { + width: "100%", + }, +}); diff --git a/mobile_app/components/primitives/index.ts b/mobile_app/components/primitives/index.ts index d6037700..b9ace578 100644 --- a/mobile_app/components/primitives/index.ts +++ b/mobile_app/components/primitives/index.ts @@ -20,6 +20,8 @@ export { default as NumericKeypad } from "./NumericKeypad"; export { Pill } from "./Pill"; export type { PillTone } from "./Pill"; +export { PreviewBadge } from "./PreviewBadge"; + export { PressSurface } from "./PressSurface"; export type { PressSurfaceProps, From 6d006755496201f0bad12b99391ee524618cecdd Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 14 May 2026 04:28:34 -0800 Subject: [PATCH 4/8] feat(primitives): add PreviewedActions wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps roadmap-preview CTAs so they look real but can't fire — pointerEvents:none shield + 0.5 opacity + transient 'not yet active' hint on tap. Light haptic on press. Pairs with PreviewBadge; consumer migrations land in a follow-up cluster. --- .../primitives/PreviewedActions.tsx | 123 ++++++++++++++++++ mobile_app/components/primitives/index.ts | 2 + 2 files changed, 125 insertions(+) create mode 100644 mobile_app/components/primitives/PreviewedActions.tsx diff --git a/mobile_app/components/primitives/PreviewedActions.tsx b/mobile_app/components/primitives/PreviewedActions.tsx new file mode 100644 index 00000000..ad729403 --- /dev/null +++ b/mobile_app/components/primitives/PreviewedActions.tsx @@ -0,0 +1,123 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { + Animated, + Pressable, + StyleProp, + StyleSheet, + Text, + View, + ViewStyle, +} from "react-native"; +import * as Haptics from "expo-haptics"; + +import { useTheme } from "@/theme"; + +const HINT_TIMEOUT_MS = 1600; + +interface PreviewedActionsProps { + /** Children rendered inside a `pointerEvents="none"` wrapper at reduced opacity. */ + children: React.ReactNode; + /** Hint label shown briefly when the outer surface is tapped. */ + hint?: string; + /** Outer surface opacity. Defaults to 0.5. */ + opacity?: number; + style?: StyleProp; +} + +/** + * Wraps roadmap-preview CTAs so they look real but cannot fire. Children are + * rendered behind a `pointerEvents="none"` shield; tapping the outer surface + * surfaces a transient "not yet active" hint and a light haptic. + */ +export function PreviewedActions({ + children, + hint = "not yet active", + opacity = 0.5, + style, +}: PreviewedActionsProps) { + const { colors, fontFamily, fontSize, radii, spacing } = useTheme(); + const [hintVisible, setHintVisible] = useState(false); + const hintOpacity = useRef(new Animated.Value(0)).current; + const timeoutRef = useRef | null>(null); + + useEffect(() => () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }, []); + + const handlePress = useCallback(() => { + Haptics.selectionAsync().catch(() => {}); + setHintVisible(true); + Animated.timing(hintOpacity, { + toValue: 1, + duration: 120, + useNativeDriver: true, + }).start(); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + Animated.timing(hintOpacity, { + toValue: 0, + duration: 240, + useNativeDriver: true, + }).start(() => setHintVisible(false)); + }, HINT_TIMEOUT_MS); + }, [hintOpacity]); + + return ( + + + + {children} + + + {hintVisible && ( + + + {hint} + + + )} + + ); +} + +const styles = StyleSheet.create({ + wrapper: { + position: "relative", + }, + pressable: { + width: "100%", + }, + hint: { + alignSelf: "center", + borderWidth: 1, + bottom: -28, + position: "absolute", + }, +}); diff --git a/mobile_app/components/primitives/index.ts b/mobile_app/components/primitives/index.ts index b9ace578..8c12cbcf 100644 --- a/mobile_app/components/primitives/index.ts +++ b/mobile_app/components/primitives/index.ts @@ -22,6 +22,8 @@ export type { PillTone } from "./Pill"; export { PreviewBadge } from "./PreviewBadge"; +export { PreviewedActions } from "./PreviewedActions"; + export { PressSurface } from "./PressSurface"; export type { PressSurfaceProps, From 8bb7008b2d9b60d4d3ddb78dcdfd63b2e3e96150 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 14 May 2026 04:29:17 -0800 Subject: [PATCH 5/8] chore(meta): add bug + feature + privacy issue templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ROADMAP § 1.4 — first-class bug intake channel was never shipped. Three structured templates: bug (device, OS, network state, repro), feature (problem framing, scope), privacy (claim, evidence, severity). --- .github/ISSUE_TEMPLATE/bug.yml | 69 ++++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature.yml | 40 +++++++++++++++++ .github/ISSUE_TEMPLATE/privacy.yml | 39 +++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/ISSUE_TEMPLATE/privacy.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..e32fbb70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,69 @@ +name: Bug report +description: Something the app does or shows is wrong. +title: "bug: " +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: What happened + description: One or two sentences. What did you see, and what did you expect? + placeholder: "Tapped Send on devnet; spinner ran 60s then 'Couldn't reach devnet' even though Wi-Fi was up." + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: Minimum steps so someone else can hit the same state. + placeholder: | + 1. Open Wallet + 2. Tap Send + 3. ... + validations: + required: true + - type: dropdown + id: device + attributes: + label: Device + options: + - Solana Seeker + - Stock Android (Pixel / Samsung / OnePlus / other) + - iPhone + - Other (note OS in the description) + validations: + required: true + - type: input + id: os + attributes: + label: OS version + placeholder: "Android 15 / iOS 18.2" + validations: + required: false + - type: dropdown + id: network + attributes: + label: Network state at time of bug + options: + - Wi-Fi + - Cellular + - Mesh only (no internet) + - Airplane mode + - Mixed / transitioning + validations: + required: true + - type: input + id: build + attributes: + label: App build / version + description: From Settings → app version, or the APK filename. + placeholder: "1.0.2" + validations: + required: false + - type: textarea + id: logs + attributes: + label: Logs / screenshots (optional) + description: Paste relevant logs or attach a screenshot. Redact addresses / seeds. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 00000000..d669525a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,40 @@ +name: Feature request +description: A new capability or UX you'd like to see. +title: "feat: " +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: The problem + description: What real-world situation triggered this? Skip "it would be nice if" — describe the gap you actually hit. + placeholder: "Sending SPL tokens isn't supported, so I can't move USDC to a friend without bridging to SOL first." + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behavior + description: Sketch the flow. Screens / states / inputs. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other ways to solve this you thought about, and why they didn't fit. + validations: + required: false + - type: dropdown + id: scope + attributes: + label: Where it lives + options: + - Wallet (send / receive / tokens) + - Messages / channels + - Mesh / network / beacon + - Settings / identity / recovery + - Onboarding + - Other + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/privacy.yml b/.github/ISSUE_TEMPLATE/privacy.yml new file mode 100644 index 00000000..34f833b7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/privacy.yml @@ -0,0 +1,39 @@ +name: Privacy / security concern +description: Something in the app, README, or behavior that you think contradicts the privacy claims. +title: "privacy: " +labels: ["privacy", "security"] +body: + - type: textarea + id: claim + attributes: + label: The claim + description: Which AnonMesh claim, doc line, or UI label looks wrong? Quote the exact wording. + placeholder: 'README says "routes through nothing" — but in mesh mode it routes through a beacon-relay.' + validations: + required: true + - type: textarea + id: evidence + attributes: + label: What you observed + description: File line, screenshot, log line, or repro steps that point to the gap. + validations: + required: true + - type: dropdown + id: severity + attributes: + label: Severity + options: + - "Misleading copy / labeling (P1)" + - "Consent gap (P0)" + - "Information leak / metadata leak (P0)" + - "Crypto / auth bypass (P0)" + - "Not sure" + validations: + required: true + - type: textarea + id: disclosure + attributes: + label: Responsible disclosure + description: For exploit-class issues, please email instead of filing here. Note your contact and we'll coordinate. + validations: + required: false From 2f95ad217a57d60342a2601b75131d3d2e11e420 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 14 May 2026 04:30:04 -0800 Subject: [PATCH 6/8] docs(readme): correct AES-128 + qualify centralized-server + relay claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns README with what the code actually ships per AUDIT § 1 R-01..R-27. - AES-256 → AES-128 + HMAC per Reticulum spec (the actual on-the-wire crypto) - "No servers" → "No centralized servers" + note community TCP relays - "routes through nothing" → peer-to-peer first, beacon-relays as fallback - Solana section: name "confidential offline transfers" as coming-soon, note mesh-RPC payload is proxied (not yet encrypted to relay) Two FALSE claims + three MISLEADING ones resolved. Remaining drift items tracked in AUDIT for follow-up. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0cee68d6..c249daa1 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ # anonmesh -**No servers. No accounts. No surveillance. No bullshit.** +**No centralized servers. No accounts. No surveillance. No bullshit.** -Encrypted mesh messaging and Solana payments for Android and iOS works on a mountaintop, in the middle of the ocean, in a blackout, or anywhere the internet doesn't reach. If two devices can find each other over *anything*, Bluetooth, LoRa radio, LAN, the internet, they can talk. No carrier. No cloud. No one in the middle. +Encrypted mesh messaging and Solana payments for Android and iOS that works on a mountaintop, in the middle of the ocean, in a blackout, or anywhere the internet doesn't reach. If two devices can find each other over *anything* — Bluetooth, LoRa radio, LAN, the internet — they can talk. No carrier required. No cloud account. Community-run public TCP relays are pre-configured for bootstrap and can be removed in settings. -You own the keys. You own the network. Nobody can take it from you. +You own the keys. You own your share of the network. Nobody can take it from you. --- @@ -14,7 +14,7 @@ You own the keys. You own the network. Nobody can take it from you. Every messaging app you use today is a surveillance platform with a chat UI bolted on. Signal requires a phone number. WhatsApp is Meta. Telegram stores your messages on their servers. Even "decentralized" apps route through centralized relays, CDNs, or DNS. -anonmesh routes through **nothing**. It uses [Reticulum](https://reticulum.network), a cryptographic transport network that works over whatever physical medium is available, and [LXMF](https://github.com/markqvist/LXMF) for message delivery. There is no account creation. There is no server to subpoena. There is no company to comply with a warrant. Your identity is a keypair that lives only on your device. +anonmesh routes through whatever's available — peer-to-peer first, beacon-relays only when no direct path exists. It uses [Reticulum](https://reticulum.network), a cryptographic transport network that works over whatever physical medium is available, and [LXMF](https://github.com/markqvist/LXMF) for message delivery. There is no account creation. There is no central server to subpoena. There is no company to comply with a warrant. Your identity is a keypair that lives only on your device. --- @@ -22,7 +22,7 @@ anonmesh routes through **nothing**. It uses [Reticulum](https://reticulum.netwo ### Direct Messages -Scan a QR code or paste an LXMF hash. That's it. The message is encrypted end-to-end with X25519 + AES-256 before it leaves your device. If the peer is offline, the message queues locally and delivers the moment a path opens, over any interface, across any number of hops. +Scan a QR code or paste an LXMF hash. That's it. The message is encrypted end-to-end with X25519 + AES-128 + HMAC per the [Reticulum spec](https://reticulum.network/manual/understanding.html) before it leaves your device. If the peer is offline, the message queues locally and delivers the moment a path opens, over any interface, across any number of hops. - No phone number. No username. No account. - Peer status (online / offline, hop count, interface) live in the thread header @@ -70,9 +70,9 @@ Non-custodial. Keys never leave the device. - Send SOL or SPL tokens to any address or mesh peer handle - Receive via QR - On-chain activity history -- Swap and yield (coming soon) +- Swap, yield, and confidential offline transfers (coming soon — roadmap previews are labeled in-app) -No analytics. No RPC phoning home beyond what's needed to read the chain and submit transactions. RPC endpoint is yours to configure. +No analytics. No telemetry. RPC calls are limited to reading the chain and submitting transactions, and the endpoint is yours to configure. On mesh-RPC mode, requests are proxied through a beacon-relay (a future release will encrypt the JSON-RPC payload end-to-end to the relay). --- From dcd60b41af00d3b65c6b36771390ac31c5b1937e Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 14 May 2026 04:30:44 -0800 Subject: [PATCH 7/8] chore(ci): add honesty-check workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greps the diff for banned theatre phrases (ARCIUM, MPC 3/3, Confidential Offline, JITO_RATE, etc.) so they can't sneak back into mobile_app/ or README.md without a PreviewBadge / coming-soon wrapper. Runs on PRs + pushes to v3. Per ROADMAP § 0.8. --- .github/workflows/honesty-check.yml | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/honesty-check.yml diff --git a/.github/workflows/honesty-check.yml b/.github/workflows/honesty-check.yml new file mode 100644 index 00000000..b1ee4f68 --- /dev/null +++ b/.github/workflows/honesty-check.yml @@ -0,0 +1,58 @@ +name: Honesty check + +# Greps the diff for theatre phrases that contradict what the app actually +# does. See ROADMAP § 0.8 + AUDIT § 2 (Category C) for the rationale. +# +# Failure means a present-tense claim about an unshipped capability landed in +# code or docs without a "PREVIEW" / "coming soon" qualifier. Either: +# - wrap the UI in / , OR +# - rewrite the copy in coming-soon framing, OR +# - if the feature actually shipped, add it to ALLOWED_PHRASES below. + +on: + pull_request: + paths: + - "mobile_app/**" + - "README.md" + push: + branches: [v3] + paths: + - "mobile_app/**" + - "README.md" + +jobs: + honesty: + runs-on: ubuntu-latest + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PUSH_BEFORE_SHA: ${{ github.event.before }} + EVENT_NAME: ${{ github.event_name }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Grep diff for banned theatre phrases + run: | + set -euo pipefail + if [ "$EVENT_NAME" = "pull_request" ]; then + BASE="$PR_BASE_SHA" + else + BASE="$PUSH_BEFORE_SHA" + fi + if [ -z "${BASE:-}" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then + echo "No base ref to diff against; skipping honesty check." + exit 0 + fi + # Banned phrases. We grep added lines only so historical text doesn't fail the check. + PATTERNS='ARCIUM|MPC 3/3|MPC-3/3|Confidential Offline|JITO_RATE|stealth mode active|routes through nothing|encrypted with AES-256' + DIFF=$(git diff --unified=0 "$BASE"...HEAD -- 'mobile_app/**' 'README.md' | grep -E '^\+[^+]' || true) + HITS=$(echo "$DIFF" | grep -E "$PATTERNS" || true) + if [ -n "$HITS" ]; then + echo "Honesty check failed. Banned theatre phrase(s) in diff:" >&2 + echo "$HITS" >&2 + echo "" >&2 + echo "Wrap in PreviewBadge / PreviewedActions, or reframe as coming-soon." >&2 + exit 1 + fi + echo "Honesty check passed." From be5df51bb896542a7602b587833cda3de06f72e6 Mon Sep 17 00:00:00 2001 From: epicexcelsior Date: Thu, 14 May 2026 16:36:28 -0800 Subject: [PATCH 8/8] chore(meta): sync package-lock to 1.0.2 --- mobile_app/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile_app/package-lock.json b/mobile_app/package-lock.json index f0a54254..3022d07e 100644 --- a/mobile_app/package-lock.json +++ b/mobile_app/package-lock.json @@ -1,12 +1,12 @@ { "name": "mobile_app", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mobile_app", - "version": "1.0.0", + "version": "1.0.2", "hasInstallScript": true, "dependencies": { "@expo-google-fonts/space-grotesk": "^0.4.1",