Skip to content

SSP Key v2.0.0 — design & UX overhaul - #101

Merged
TheTrunk merged 22 commits into
masterfrom
v2.0.0
Aug 6, 2026
Merged

SSP Key v2.0.0 — design & UX overhaul#101
TheTrunk merged 22 commits into
masterfrom
v2.0.0

Conversation

@TheTrunk

@TheTrunk TheTrunk commented Jul 17, 2026

Copy link
Copy Markdown
Member

What this is

The complete v2 redesign of SSP Key on the shared SSP design system (amber tokens, bundled Inter with real weights on both platforms, Lucide icons, the pillar mark) plus major approval-UX and security additions. 9 commits, reviewed through multiple adversarial audit rounds.

Highlights

The approval moment

  • Unified request template across all 11 request types with slide-to-approve (release-based, spring-back on early release, accessibility activate path)
  • On-device calldata decode extended: approve / increaseAllowance / setApprovalForAll with high-severity warnings for unlimited approvals and collection-wide operator grants (selectors verified by keccak-derived test vectors)
  • Risk banners single-sourced (VaultRiskStrip delegates to RiskBanner)

Pairing & security

  • Multi-chain batch sync: one approval activates N chains (versioned v1 payload, fail-safe on unknown versions, duplicate chain/xpub rejection)
  • Pairing verification words (byte-identical derivation with the wallet) + scan-to-verify; mismatch banner passes AA contrast
  • Real word-verification on Create — 3-word challenge with BIP39 decoys replaces the "I backed it up" switch (read-only over the phrase)
  • Signed-action History: local, encrypted, biometric-gated log of co-signed actions (public metadata only, never synced)
  • Privacy mode masking History identities (MMKV, append-only key)

Structure & platform

  • Home.tsx decomposed 4,132 → 1,465 lines (usePendingRequests hook + render split), zero behavior change proven by token-level parity
  • Inter bundled with a proper Android weight map (res/font XML + ReactFontManager — no faux bold); Lucide via pure JS
  • react-native-asset retired and react-native-vector-icons removed: APK −6.4 MB
  • Reduced-motion support; warm-stone theming; platform-correct monospace; tabular numerals

Verification

  • 386 unit tests green; tsc/lint/prettier clean
  • Fresh native builds verified: Android assembleDebug + emulator install/launch (clean logcat), iOS simulator build + visual Inter confirmation
  • Zero changes to signing, key derivation, seed handling, or keychain encryption (independently audited; decomposition parity token-verified)

Before merge

  • Physical device pass per DEVICE_SESSION_PLAN.md: every request type over the socket, tap/slide behavior, batch sync round-trip with the wallet, verification-code match, biometrics, push notifications, install-over-v1 upgrade
  • Pairs with ssp-wallet v2.0.0 (protocol compatible both directions with v1)

TheTrunk added 9 commits July 15, 2026 23:08
Generalizes the trustless-decode vault pattern to all signing: identicon header,
plain-language decoded action, anti-poisoning recipient, risk banner, raw hex
behind Advanced, human-readable ERC-20 calldata. Slide-to-approve replaces the
approve button across all 10 request types; decode failures now fail closed.
Batch approval derives per-chain keys with progress and returns them via the
existing sync path. Adds the out-of-band 6-word verification code with word +
scan-to-verify comparison so a malicious relay cannot swap extended keys.
A co-signer that remembers what it signed: a local, encrypted, biometric-gated
log of every action this Key co-signed. Uses a new dedicated keychain service
and the app's existing encryption; stores only public metadata (never
signatures), never synced to the relay. Recorded via one additive fire-and-forget
call after the relay post commits — no signing logic changed.
… audit fixes

Recheck fixes: pin @noble/hashes 2.2.0 explicitly; batch chain-sync rejects
the same xpub reused across chains; verification-chip accents on the SSP
palette with black badge text (byte-identical with ssp-wallet); removed
console logging of sync payloads.

Phase 5: real word-verification on Create — a 3-word challenge with BIP39
decoys replaces the 'I backed it up' switch (read-only over the in-memory
phrase; storeMnemonic unchanged, runs only after verification). PillarMark
brand component on Startup and backup-confirmed moments. Platform-correct
monospace (Menlo on iOS), tabular numerals on amounts/fees, fixed heights
relaxed for all 41 languages.

Phase 6: privacy mode — masks Signing History identities/txids/identicons
(fixed-length dots; new append-only MMKV key), toggled via History eye,
identity-chip tap, or Settings switch; never masks anything inside signing
screens. VaultRiskStrip now delegates all banner presentation to RiskBanner
so severity styling lives in one place (external API unchanged).
Home.tsx shrinks 4,132 -> 1,465 lines with zero behavioral change, proven by
token-level parity checks against the previous revision at every stage:
usePendingRequests hook owns the 13 pending-request states and the socket
mapping effects (identical API, subscription untouched); the render splits
into HomeProgress/HomeIdle/HomeRequests/HomeModals with Home remaining the
sole state owner; 15 handler bodies relocate verbatim into action modules
behind a context whose property names equal the original closure variables;
pure input parsing extracted with tests. One verified consolidation: the
vault-signing ingestion block that existed as two verbatim copies is now
ingestVaultSigningRequest.

Icons: the Feather + MaterialCommunityIcons mix migrates to lucide-react-native
(pure JS on the existing react-native-svg; exact-pinned). All sizes, colors and
accessibility props preserved. react-native-vector-icons stays as a dependency
until a device-verified native rebuild removes its fonts.

Device pass before release should exercise: socket delivery of every request
type, pull-to-refresh vault-sign ingestion, pairing + batch sync end-to-end,
approve and reject paths, QR + manual input parsing, nonce replenish.
Official Inter v4.1 static TTFs (Regular/Medium/SemiBold/Bold) wired through
the RN asset pipeline. iOS registers all four faces via UIAppFonts so
fontWeight resolves to real weights. Android gets an XML font family
(res/font/inter.xml, weights 400-700) registered through
ReactFontManager.addCustomFont — RN's assets/fonts path cannot resolve
500/600 weights and would faux-bold, so the XML family is the correct route.
Fonts.ts sets the Inter family with an unchanged API; no component changes.

Verified: iOS simulator build launched and letterform-matched to Inter Bold;
Android assembleDebug + emulator install ran clean (visual bold check pending
a human eye — FLAG_SECURE correctly blocks screenshots).
calldataDecode gains increaseAllowance(address,uint256) and
setApprovalForAll(address,bool) alongside the existing approve. Allowances at
or above 2^255 flag unlimited (dapps use many astronomical sentinels; nothing
legitimate reaches 2^255) while transfers keep the exact-max sentinel;
setApprovalForAll accepts only canonical bool encodings and fails closed.
The approval screen warns at high severity for unlimited approvals and
collection-wide operator grants, naming the spender; bounded approvals get an
info line. Human token amounts only when the on-device registry knows the
decimals — never guessed. Decode-layer only; signing untouched; existing
decode tests pass byte-unmodified.
CRITICAL: the setApprovalForAll selector was a22cb442; the real keccak-derived
selector is a22cb465 — the collection-approval warning never fired on real
calldata. Fixed, and the test vectors now DERIVE selectors via keccak at test
time so a wrong constant fails tests instead of being self-confirmed by
vectors built from the same constant.

Also: approve(spender, 0) reads as a revocation instead of 'spend 0 units';
EVM-signing request handler no longer logs the request body (carried the
wallet's partial signature); signed-tx/txid logs removed; sign-history appends
serialized against concurrent approvals; Create resets its challenge flags on
success; verification words deliberately monospace (cross-device parity with
the wallet's mono rendering); mismatch banner black-on-red passes AA;
PillarMark honors reduce-motion; generic spinners replaced with the pillar
brand loader; creation stepper themed to warm stone; dead font styles removed.
…~6.4 MB)

Retire the react-native-asset pipeline: Android resolved Inter exclusively via
res/font/inter.xml, so the assets/fonts copies were dead weight; fonts are now
manually wired on both platforms (documented in assets/fonts/README.md) and a
stray react-native-asset run can no longer reintroduce duplicates.

Remove react-native-vector-icons entirely — zero imports remained after the
Lucide migration; the gradle fonts hook, Info.plist icon-font entries and
pbxproj references go with it. APK shrinks 169.9 -> 163.2 MB. Verified with
fresh builds: Android assembleDebug + emulator install/launch with clean
logcat; iOS simulator build + screenshot (Inter renders, bundle carries
exactly the four Inter TTFs and no icon fonts). 386 tests green.
@TheTrunk TheTrunk changed the title V2.0.0 SSP Key v2.0.0 — design & UX overhaul Jul 17, 2026
TheTrunk added 13 commits July 17, 2026 09:52
7 Dependabot alerts -> 2 accepted-risk (elliptic — no patched version exists;
signing-adjacent, untouched). Fixed: ws family (ethers 6.17.0 upstream
security release, deduping with the Schnorr SDK's copy, + scoped resolution
for viem), uuid via jayson (scoped resolution, smoke-tested), concurrent-ruby
build tooling. Safe JS minors (navigation, axios, i18next, toast); zero
native-module bumps — pod install verified a byte-for-byte no-op; jest bump
reverted (preset incompatibility). @noble/hashes stays 2.2.0 (wallet parity).
386 tests, Android assembleDebug, and a Hermes release bundle all green.
versionCode 60, iOS build 9, marketing version 2.0.0 on both platforms.
The seed-backup modal (the last v1 surface) is rebuilt around a shared
SeedPhraseBackup component: high-severity RiskBanner instead of a red text
wall, a numbered monospace word-chip grid masked until revealed, proper
button hierarchy — shared by Create, Restore, and the key-details reveal.
Success moments (sync, tx sent, EVM sign, nonces) share a SuccessHeader
with the pillar mark and a green check; menu rows get icons, left-aligned
labels, 44pt targets and menuitem roles; Delete SSP Key is styled as the
destructive action it is; Scanner fixes a theme dispatch running in the
render body, respects reduce-motion, and makes permission messages legible;
Authentication and eye toggles become labeled touchables; sensitive values
render monospace everywhere. Radii and dividers snapped to tokens; a11y
labels across icon-only controls. Presentation only — reveal, decrypt and
signing logic untouched.
The iOS LaunchScreen is rebranded to the v2 identity: the centered pillar mark
on the brand-dark background (#131314, matching the app splash) replaces the
white background and system-font 'SSPKey' text. Fixes a latent filename-case
mismatch in the SplashIcon asset (iTunesArtwork vs ItunesArtwork) that would
blank the splash on a case-sensitive build.

Settings is redesigned from a flat modal into a polished grouped-card screen
mirroring the wallet's v2 Menu: Preferences (language shown concisely as
'System' / native name, and a compact amber pill privacy toggle replacing the
oversized native switch) and Security (change password; Delete SSP Key Data as
the destructive bottom action) up front, the relay and per-chain network
endpoints tucked into a collapsed Advanced expander with uppercase section
labels and recessed fields identical in both themes, and an About card with
the version and Powered by Flux. Consistent type scale, 44pt rows, aligned
icons and chevrons throughout. Modal-preserving; every save/reset/delete/
change-password/language/privacy handler is byte-identical.

386 tests, tsc, lint, prettier green; every state verified on the iOS
simulator through a design-critique loop (light + dark).
Mirrors the wallet's periodic seed-backup reminder, adapted for the Key: a
routine 30-day checkup card on the Home idle screen — pure security framing,
no balance (the Key shows none). It renders ONLY when a Key is set up (a seed
exists) and is due, so it never appears on Welcome or during onboarding;
verify resets the 30-day cycle, dismiss snoozes it. Create and Restore stamp
the first verification so fresh users are not asked immediately. Verify runs
the existing word-challenge behind the biometric/password gate over a
read-only seed decrypt (reusing Authentication + ConfirmSeedWords + the
key-details reveal path — no new crypto). Also adds an on-demand 'Verify seed
backup' row in Settings -> Security. Pure cycle logic + storage are unit
tested. 398 tests, tsc, lint, prettier green; verified on the iOS simulator
(absent on Welcome, present on Home idle for a set-up Key).
…ling

- Drop the cross-chain duplicate-xpub rejection: all testnets share slip 1,
  so Sepolia and Amoy (both evm/p2sh with default bip32 bytes) derive
  byte-identical wallet xpubs — as do Testnet and Signet Bitcoin. Any batch
  containing such a pair was rejected wholesale as 'duplicate_xpub'.
  Duplicate CHAIN entries remain invalid.
- Post chainsyncrejected('invalid') for malformed requests instead of
  silently dropping them — the wallet otherwise waits the full 30s fallback
  timeout before offering per-chain QR sync. This wires up the previously
  dead 'invalid' rejection-reason arm.
- Declining a batch left batchStartedRef stuck true for the session
  (suppressing every later verification code) and, on a fresh pairing,
  dropped the identity verification code entirely while the wallet shows
  one. Decline now shows the identity code and resets the flag.
- CHAIN_SYNC_POST_SPACING_MS 1500 → 3000: the relay sync doc is
  last-write-wins per walletIdentity and the wallet polls at 1s — 1.5s left
  almost no margin for network jitter on already-derived chains, and a
  missed doc is never re-posted. 3s gives the poll 2-3 chances per doc and
  stays far inside the wallet's 60s stall window.
Round-3 security sweep — the SSP Key is the second factor whose approval
screen is the last line of defense, so display must equal what is signed.

- EVM vault: the Schnorr sign consumed rawUnsignedTx (an opaque hash the
  SDK's fromJson trusts and never recomputes) while the approval screen
  displayed a decode of a SEPARATE evmUserOp (tx) or signMessage
  (personal_sign). Nothing bound them, so a compromised wallet could show a
  benign operation and have the key co-sign a vault-draining one. New
  lib/userOpVerify.ts recomputes the UserOp hash (aa-core
  getUserOperationHash) or the EIP-191 message digest (ethers hashMessage)
  on-device from what was DISPLAYED and refuses to sign on mismatch — parity
  with the Solana vault's existing decode-what-you-sign hard-block.
- UTXO vault: the key adds its SIGHASH signatures over walletSignedHex's
  outputs but the approval screen decoded rawUnsignedTx. Now asserts the two
  transactions have an identical output set (value + script) before signing,
  and fails closed when rawUnsignedTx is absent.
- Consumer UTXO approval (decodeTransactionForApproval) overwrote the shown
  receiver/amount on every output, so a multi-output tx displayed only the
  LAST recipient and understated the total sent. It now sums all non-change
  recipients into the amount and reports recipientCount; the approval screen
  shows a warning when the tx pays more than one destination.
- Scan-to-verify forced persisted dark mode: Scanner dispatches darkMode:true
  on mount and only Home's scanner paths restored it — the theme slice is
  redux-persisted, so a light-theme user who tapped 'Scan wallet to verify'
  was stuck in dark mode across restarts. VerificationCode now restores the
  theme on scan and on close, and mounts the scanner full-screen.
- 'Delete SSP Key data' also clears the encrypted sign-history blob (keychain
  service sspkey_sign_history) — the wipe contract says all data.
Password recovery
- The released key comes from its own BIP-48 account, published with this
  device's signature on sync so the wallet can verify it independently.
- Each request is answered once, is time-bounded, and names the key index it
  needs; the approval screen shows a six-word code matching SSP Wallet.

Approval screens
- Totals and recipient counts cover every output in a proposal.
- Any decode failure fails closed, with Reject still reachable.
- Slide-to-approve stretches correctly in every request wrapper.

Sync
- One nonce pool per batch chain sync; relay actions route identically over
  the socket and poll transports.

Copy pass across the request, home and create/restore screens.
Ignore regenerable screenshot artifacts.
Zcash NU6.3 consensus branch id support; ships together with the matching
ssp-wallet bump.
Google Play + App Store copy refreshed to v2 messaging (2-of-2 story,
Halborn audits, accurate chain list); field limits verified. Screenshot
panels regenerate via the documented pipeline (fastlane/screenshots is
gitignored per RN convention).
@TheTrunk

TheTrunk commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Since opening, this branch additionally picked up:

  • approval-screen design unification + slide-crash fix (device-verified)
  • chain-sync batch activation support
  • @runonflux/utxo-lib 1.0.5 (Zcash NU6.3) — ships together with ssp-wallet
  • store listing metadata (fastlane layout)

@TheTrunk
TheTrunk merged commit ed8ab55 into master Aug 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant