Skip to content

Sun-lit redesign: new logo, neumorphism, global illumination - #1324

Closed
takemiyamakoto wants to merge 48 commits into
masterfrom
release/sunlit-redesign
Closed

takemiyamakoto wants to merge 48 commits into
masterfrom
release/sunlit-redesign

Conversation

@takemiyamakoto

@takemiyamakoto takemiyamakoto commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Redesigns the iOS app around the approved winged-sun logo, with neumorphic surfaces, one light across every screen, and animations that report real state. Owner request, 8 November 2026.

This is UI only. Key generation, storage, signing, seed handling, networking, remote config and chain logic are unchanged.

  • Logo: the approved winged-sun badge replaces the old mark in the app icon, launch screen and in-app assets. The generator copies the master's path text, so the wings stay byte-identical (scripts/brand/generate-ios-brand-assets.mjs, 100 outputs checked against a manifest).
  • Full-page screens: PIN, welcome, onboarding start, About and launch show the wings at about 90% of the page width without the circle, as the owner asked. The wallet hero keeps the round sun because its sync arc wraps it.
  • Design system: fearless/Common/DesignSystem/Sunlit holds the shared light, raised surfaces, inset controls, the tab well and the sheet rim. Screens use it instead of raw colours.
  • Animation: each animation answers an event: press, balance or price change, sync running, finished or failed, a new list, tab change, transaction state, wrong PIN, PIN digit, copied. Reduced Motion is honoured, and recovery and signing screens stay still.

Related Issue

No tracked issue.

Merge after #1315. This branch contains hotfix/4.2.0-polkaswap-usability at 23ecb96e6, so until #1315 lands the diff against master includes it. The redesign alone is 239 files, +5,788 / -2,540.

Target Branch

  • This PR targets develop
  • This PR targets master and is a release or hotfix PR

The head is named release/* because validate only lets release/* and hotfix/* into master. It does not change the app version.

Type of Change

  • feat (new feature)
  • fix (bug fix)
  • refactor (no functional change)
  • chore/build (tooling, CI, deps)
  • docs

Screenshots / Videos

Before and after screens are in the workspace at build/mobile-redesign-20261108/: wide-wings-final-screens.png, ios-final-b16/ and ios-final-b17/. The commit, results and binary hashes are in FINAL-RECEIPT.json there.

Test Plan

Application and behavioral tests ran locally on 488fd4be7. The final head d3b3d5166 adds only whitespace in one test file and explicitly records two generated TON documentation placeholders in the TODO baseline; app sources and test non-whitespace content are unchanged. On 10 November the retained summaries, logs, simulator binaries and screenshots were rechecked against their SHA-256 receipts (19/19 match).

Check Result
Full fearlessTests, iOS 27.0 simulator 3,814 passed, 0 failed, 95 skipped of 3,909
Focused redesign suites, iOS 27.0 59 of 59
Targeted classes, iOS 18.4 179 of 179
Profile continuity, 8 repeats of 9 tests 72 of 72
Brand assets against the manifest 100 outputs match
SwiftLint 0 errors, 71 warnings
Release audit list 25 retained passes; previously failing TODO audit now passes
Pinned SwiftFormat 0.47.13 2,825 app files and all 84 changed Swift files pass
TODO audit regression fixtures pass with ripgrep and grep fallback
  • TODO baseline: records the two existing generated comments for AccountEvent.extra from the pinned TonAPI schema. Generated sources remain byte-identical; the audit still rejects new debt and crashing placeholders.
  • Formatting: validated with the same pinned SwiftFormat used by Xcode. The remaining test-file changes are whitespace only, verified against the tested commit.
  • Walked on the simulator: launch, welcome, PIN at normal and largest text, wallet, settings and About on the final builds. Send, Receive and transaction states were walked on an earlier build of this branch.

Not verified: physical devices, a signed release build, and the full suite on iOS 18 (targeted classes only).

Risks & Rollout

  • No migration, new configuration or secrets. Rollback is a revert.
  • Not part of iOS 4.2.1, which stays on hotfix/4.2.0-polkaswap-usability. This ships in the next version.
  • Master requires linear history, so this needs a squash merge.

takemiyamakoto and others added 30 commits November 7, 2026 10:48
Two Polkaswap screens broke on iPhone with the 4.2.0 redesign (#1305):

- The token picker stretched its search field over most of the sheet
  and left the list a strip at the bottom. SearchTextField took its size
  from BackgroundedContentControl's empty contentView, a zero height,
  and #1305 replaced its fixed height with a 44 pt minimum, so the
  sheet's stack view stretched the field. The field now reports its own
  height and hugs it, and the list and its empty state share a container
  so the hidden empty view no longer fights the list's height.
- Preview pushed the confirmation inside the new swap tab, where the tab
  bar and the raised Polkaswap button covered Confirm and the header sat
  under the status bar. The confirmation now hides the tab bar and keeps
  its header and button inside the safe area.

Both tests fail on the previous code: the search field measured 653 pt
in an iPhone-sized sheet, and a tap on Confirm reached a tab button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 4.2.0 a swap quote could take minutes to appear. Since #1305 every
quote request carried its own ID and only the newest request's reply was
shown, while every SORA block (about 6 s) cancelled the pending request,
hid the shown quote and asked again. Any reply slower than a block was
therefore thrown away, block after block, until one happened to be fast
enough. Quotes also queued in the app-wide operation queue behind
unrelated work, waited 0.8 s after each keystroke, and replaced block
subscriptions kept refreshing because the subscription service cancels
them as requests, which leaves them running on the node.

- A reply is shown when it answers the question on screen (pair, amount,
  direction, market) and nothing newer is shown, so a refresh no longer
  invalidates the reply already on its way. Replies to an old amount and
  older replies are still dropped.
- A block refresh keeps the shown quote, sends nothing while a reply is
  awaited, and a failed refresh keeps the quote instead of clearing the
  inputs behind an alert.
- Quotes go straight to the SORA socket with a 10 s timeout; fees and pair
  checks use the swap screen's own operation queue.
- Typing waits 0.3 s instead of 0.8 s.
- Updates from replaced block subscriptions are ignored.

In the simulator, against the production SORA nodes, the time from
keystroke to quote fell from about 1.8 s to 0.7 s. The new presenter test
fails on the previous code: the awaited reply is discarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026.11.5 went to TestFlight and App Review on 4 November 2026, so it
joins the consumed builds and becomes a supported in-place upgrade
predecessor in the usability gate. The startup capture keeps accepting it
under its own name, every release tool now pins 2026.11.6, and the
build-number check's test treats 2026.11.5 as the latest consumed build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apple approved 4.2.0 with build 2026.11.5 (Pending Developer Release on
7 November 2026), which closes 4.2.0 to new uploads, so the Polkaswap
fixes ship as 4.2.1 (2026.11.6). The Release target, the archive,
identity, signed-artifact and build-number checks, the usability gate and
fastlane now expect 4.2.1. The startup capture still expects 4.2.0 for
every earlier build and 4.2.1 only for 2026.11.6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The self-test parsed the check's output with a literal "Release 4.2.0"
and used 4.2.0 in its version-drift cases, so it stopped recognising a
passing repository once the release moved to 4.2.1. It now reads the
version and build from the check's output and uses them in every case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the owner's word, 4.2.0 (2026.11.5) was removed from review, its version
record renumbered to 4.2.1 with build 2026.11.6, and submitted on
7 November 2026 at 10:06 UTC. It is Waiting for Review with a manual release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify-native-source.py accepted SOURCE_PACKAGES_DIR from its callers but never
read it, so a moved tag or an edited package checkout passed the check. It now
compares every pinned checkout's HEAD with its revision and refuses local
changes, using git --no-optional-locks so the check never writes.

verify-pods.py and config/pods-source-pins.json record digests of the 11 pods
compiled into the app or its tests and of the rswift and swiftformat
executables that Xcode runs. The rswift digest equals the upstream 6.1.0
release binary. Recorded from the reviewed 4.2.1 install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
createPublicKeySoft copied SR25519_KEYPAIR_SIZE (96) bytes out of a 32-byte
stack buffer, reading stack memory into the key and always failing. Nothing
calls it today; it now copies the 32-byte public key and a test checks it
against the soft-derived key pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tonkeeper/ton-api-swift 0.1.7 has no licence file. Packages/TonAPI holds the
same client generated here by Apple's swift-openapi-generator 0.3.1
(Apache-2.0) from the MIT-licensed opentonapi specification, with provenance
in its README. Client.swift is byte-identical to ton-api-swift's; Types.swift
is byte-identical after postprocess.py drops Action._type, as ton-api-swift
did, so a new tonapi action kind cannot fail a history page.

scripts/deps/regenerate-tonapi.sh rebuilds the pinned generator and fails if
the committed sources differ from fresh output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner, 2026-11-07: fix the key-exposure audit findings, and "don't use any
closed source libraries. make everything you need here in this repo, open
source".

Closed and non-open-source code:
- Delete the closed Kotlin/Native sorawallet framework, loaded at every
  launch. Its only use, Polkaswap strategic-bonus APY, is now Swift
  (PolkaswapAPYWorker, PolkaswapPoolApyMapper) against pi.soramitsu.io; the
  framework's SubQuery endpoint answers 503.
- Remove WalletConnect (reown-swift, WalletConnect Community License) with its
  prebuilt yttrium binary and five Reown-only packages. A one-time clean-up
  deletes only com.walletconnect.sdk generic passwords and WalletConnect
  defaults. Keep the Chainlink ABI code under its web3swift and MEWwalletKit
  licences.
- Build against the in-repo TonAPI package instead of ton-api-swift.

Key exposure:
- The emergency recovery export becomes a password-encrypted file (scrypt plus
  XSalsa20-Poly1305, 8+ characters, verified before sharing) and never shows a
  secret; scripts/recovery/decrypt-emergency-material.py opens it.
- Forget wallet (now PIN-gated) and logout erase the Keychain secrets of
  removed wallets that were backed up, after their rows are gone. Orphaned keys
  and never-backed-up wallets' keys stay recoverable through the encrypted
  export.
- PIN lockout with backoff persisted in the Keychain, constant-time compare,
  biometric enrollment checks that trust an existing install's first unlock,
  a 5-minute monotonic background re-lock, and PIN or biometrics before every
  fund-moving or staking submission.
- JSON exports need 8+ characters; secret files get complete protection, their
  own directories and removal after sharing; Inbox copies and old leftovers are
  removed.
- Remote SVGs are filtered before SVGKit; seed and key share sheets exclude
  Copy; secrets hide while the screen is captured and screenshots warn;
  keyboard extensions are refused.
- TON Connect never signs a ton_proof for a site its manifest does not come
  from, and gets a veto-only kill switch.
- Xcode now runs the checkout and pod verification before any build tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026.11.6 is in App Review and counts as consumed; 2026.11.7 replaces it in
review with the closed-source removal and the key-exposure fixes. Release
tools, gates and their tests now pin 4.2.1 (2026.11.7), as cc8e3ec and
1092c5e did for 2026.11.6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…26.11.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Packages/FearlessWalletConnect speaks WalletConnect v2 in the wallet role on
Apple's system frameworks only (Foundation, CryptoKit, Security, Combine). It
replaces Reown's reown-swift, which is under the WalletConnect Community
License and is not open source.

- X25519 key agreement, HKDF-SHA256 session keys, SHA-256 topics, and
  ChaCha20-Poly1305 type 0 envelopes in padded base64 (type 1 is read, never
  opened).
- The relay's EdDSA did:key sign-in JWT, valid for 24 hours.
- The relay over URLSessionWebSocketTask: subscribe, batch subscribe,
  publish, unsubscribe, the batch message fetch, acknowledging each delivery,
  keep-alive pings and reconnecting with back-off.
- Pairing, proposals, approval and refusal, settlement, requests checked
  against the session's namespaces, answers, ping, disconnection and expiry,
  with each Sign method's relay tag and lifetime.
- Verify: ES256 attestations from the relay, or the trusted service's answer
  by message hash.
- A strict JSON reader that keeps numbers exact and refuses duplicate keys.
- Its record in the app group's user defaults and its keys in one keychain
  item, and the import of what WalletConnectSwiftV2 (Fearless up to 4.1.0) and
  Reown 1.8.0 (4.2.x) saved, left in place for a downgrade.

Tests run on macOS with swift test: RFC 7748 and WalletConnect relay-auth
vectors, the wallet role against an in-memory relay that follows the live
relay's delivery rules, and both earlier storage layouts. Two checks use the
WalletConnect SDK installed outside the repository, never as a dependency:
scripts/walletconnect/reference-vectors.mjs for a differential cryptography
check, and reference-dapp.mjs as a dApp on the live relay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner, 2026-11-08, asked "Bring WalletConnect back on the new 4.2.1 head
with this client?": "Bring it back".

e0dda28 removed WalletConnect because reown-swift is not open source.
This restores the WalletConnect part of that commit on
Packages/FearlessWalletConnect and keeps everything else in it: the
key-exposure fixes, the removal of sorawallet and ton-api-swift, and the
source and pod verification phase.

- The WalletConnect service, screens, coordinators, models and tests come
  back as they were in 4.2.1 (2026.11.6), on FearlessWalletConnect instead
  of WalletConnectSign. Reown's socket engine, socket factory and crypto
  provider stay deleted.
- Forgetting a wallet and logging out disconnect its WalletConnect sessions
  again, after the new secret erasure.
- RetiredWalletConnectDataCleanup is removed. It deleted the
  com.walletconnect.sdk Keychain items and WalletConnect defaults that the
  client imports, so sessions paired in 3.0.5 to 4.2.1 (2026.11.6) carry
  over again. Devices that already ran 2026.11.7 lost them; their dApps
  pair again.
- RLPBigInt.swift and SignTypedData.swift (MEWwalletKit, MIT) return to
  WalletConnect/eip_mew with that licence and a NOTICE entry; the regex
  helper the ABI move dropped is String+Match.swift there.
- The release audits again require WalletConnect.projectId
  (FL_WALLET_CONNECT_PROJECT_ID) and check that the WalletConnect service
  uses the entitled app group.
- No package is added: the resolved graph stays at 40 external packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/walletconnect-protocol.md describes Packages/FearlessWalletConnect:
its files, the wire format, its storage, the import of what earlier SDKs
saved, and how to rerun its checks, including the real-storage upgrade
check from a Reown 1.8.0 build. It notes that 4.2.1 build 2026.11.7
deleted those records once per install, so a device that ran that build
has nothing to carry over and its dApps pair again.

The binary provenance record says the wallet now speaks WalletConnect
with its own client on Apple's system frameworks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WalletConnect pairing codes use the `wc:` scheme (EIP-1328), but the QR
matcher accepted `ws:` instead, so a scanned pairing code was reported as an
invalid QR code. `WalletQRService` now matches `wc:` codes itself, from the
camera and from gallery images, and no longer reports a `ws:` node address
as a pairing code (the shared decoder still does; its result is refused).

A new connection is only useful for signing, so `connect(uri:)` opens it
together with signing: while `WalletConnectSigningPolicy` is off, a scanned
code shows a localized "not available in this version" message and nothing
is paired.

This change also adds, in every bundled localization, the strings the
reworked WalletConnect request flow uses in the following changes.

Tests cover pairing codes and node addresses from the camera, the gallery and
the scanner, connecting with signing off and on, malformed codes, and the new
message in every shipped localization. The scanner tests now use `wc:` codes.

Ported onto the in-repo client (Packages/FearlessWalletConnect): the service
pairs through the client, or through an injected pairer in tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 805b624e2cadaa59856d89ea719e56ba9230e7b6)
…ation

Every request is now prepared before its confirmation opens, and the
confirmation and the signer use the same prepared values:

- The request must name an account the session approved for the request's
  chain. The signing key is that account's, not a match picked by address
  from all wallets.
- EVM transactions get their nonce, gas limit and gas price before the
  confirmation opens. The confirmation shows the network first, then the
  recipient (the token recipient or spender for ERC-20 transfers and
  approvals), the amount and the most the network can charge, and exactly
  those values are signed; nothing is quoted again after Confirm. Chain ids
  are parsed as the decimal values the app stores, a transaction for another
  chain is declined, and so is a request without a recipient.
- EIP-712 typed data must carry the request's chain id when its domain names
  one, and permits show their spender and amount. The legacy array format,
  which no domain binds to a network, is declined.
- Substrate transactions must carry the chain's genesis hash, spec version
  and transaction version. The call is decoded and must encode back to
  exactly the dApp's bytes; transfers show their recipient and amount, the
  fee is quoted for that exact extrinsic, and the tip is signed as sent.
- Substrate messages are signed inside the `<Bytes>…</Bytes>` envelope that
  polkadot-js uses.
- Personal-sign and typed-data signatures use the 27/28 recovery id that
  JSON-RPC callers and `ecrecover` expect.

Declined requests show a localized reason and get an error response, and the
request sheet no longer prepares the request again on every balance update.
`WalletConnectSigningPolicy.production` stays off.

Tests cover account binding, the quoted and exactly signed transaction for a
decimal chain id, chain-id, address and quantity checks, ERC-20 decoding, the
EIP-712 specification's example digest, permits, the `<Bytes>` envelope and
its signature, Substrate genesis, runtime and call checks against the bundled
Polkadot runtime, the signed payload, a runtime change before signing, and
the confirmation rows for every method.

Ported onto the in-repo client: the new files import FearlessWalletConnect,
and the project changes were applied to this line's project file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 59c74e0bf33f93f29593006dc1219cb4b39e2b08)
…sts behind the lock

- Signing now waits for the PIN or biometrics, asked from the confirmation
  after the user taps Sign. Closing the PIN screen signs nothing and leaves
  the request open to sign or reject; without a PIN nothing is signed, and a
  PIN screen that goes away without an answer counts as not confirmed.
- The PIN screen that locks the app after inactivity now records that the
  app is locked. While it is shown, WalletConnect requests and notices wait
  in their queue, and a request already on screen is hidden until the app is
  unlocked.
- A request the SDK delivers again while it is queued or on screen is shown
  only once.

Tests cover signing with and without the user's confirmation, the PIN screen
from the confirmation with its success and cancel paths, the missing-PIN
case, requests waiting behind the lock and appearing after it, one request
at a time, a repeated request shown once, the request window hidden while
locked, and the lock ending with its PIN screen.

Applied unchanged on the 4.2.1 line: the lock marker sits after the head's
unlock-PIN guard in FearlessApplication.dropSession().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit cc3e7ee3a6275c65e84f58f077de2781bcd66ebe)
…s scams

The SDK reports what WalletConnect Verify knows about the app behind each
proposal and request, but the service dropped it. It now reaches the
screens:

- The proposal shows a Verification row below the app: verified domain,
  domain not verified, or domain mismatch.
- The request sheet shows the same status under the app's name; a domain
  mismatch replaces the generic warning with one saying the app may not be
  the website it claims to be. The confirmation lists it as its last row.
- A proposal or request from an app Verify flags as a known scam is declined
  before anything opens, and the user is told which app was blocked and that
  nothing was connected or signed. This applies while signing is off too.

The notice presenter now serves both the signing-unavailable and the
blocked-app notices. The domain-mismatch status is shortened for the rows,
with its explanation as a separate warning, in every bundled localization.

Tests cover how every Verify result maps to what is shown, declined and never
shown scam proposals, scam requests declined before any confirmation with
signing on and off, verified requests opening with their status, the
blocked-app notice, the confirmation row, and the texts in every shipped
localization.

Ported onto the in-repo client: the client's sessionProposal and
sessionRequest events carry the VerifyContext its Verify client produced, and
a scam-flagged proposal is refused through the client (or an injected
rejecter in tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1b9a737d14a75b715d8f3d722c7a682b8dd9dc18)
…submission PIN

Owner's decision for 4.2.1 (2026.11.8), relayed from the key-exposure
session: "we need some wallet connect support of course", choosing
"Connect and sign": pair from wc: codes and links, sign EVM, Polkadot and
TON requests, ask for the PIN or Face ID before each signature, show the
real requesting site, and let a remote kill switch turn signing off.

- WalletConnectSigningPolicy.production is on. The ported WC-SIGN checks
  stay in front of every key: the account the session approved, the
  network, the decoded call that must encode back to the dApp's bytes, the
  quoted fee, the <Bytes> envelope, and scam-flagged apps declined.
- Every signature asks through SubmissionAuthorizer, the step every
  fund-moving and staking submission takes, with its PIN lockout and
  biometric rules. Without a PIN nothing is signed.
- Remote vetoes, absent means enabled and only an explicit false pauses:
  evm_dapp_signing_enabled and polkadot_dapp_signing_enabled for
  WalletConnect, ton_dapp_signing_enabled for TON Connect transactions and
  ton_proof sign-ins, the Android key names; never dapp_enabled. A paused
  request is declined before anything opens and the user is told nothing
  was signed. The confirmation checks again before and after the PIN.
- The requesting site is shown as WalletConnect Verify reports it: on a
  domain mismatch the website row names the site the request came from,
  and a site Verify could not check is labelled "Domain not verified"
  with a warning.
- wc: links and fearless://wc?uri= links pair (Info.plist registers wc).
  A link that opens the app waits until WalletConnect starts after the
  unlock; a failed link gets a notice. A wc: link without a key only
  brings the wallet forward.
- New strings in every bundled localization.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/walletconnect/reference-dapp-sign.mjs runs the WalletConnect SDK
most dApps ship as a dApp on the live relay. It asks for personal_sign,
eth_signTypedData_v4 and polkadot_signMessage, and checks every signature
with code that is not Fearless's: ox recovers the EVM signer and
@noble/curves verifies the Polkadot ed25519 signature over the <Bytes>
envelope (and that it does not cover the bare message). The SDK and those
libraries stay outside the repository.

WalletConnectLiveSigningTests (skipped unless FEARLESS_WC_SIGN_LIVE=1)
is the wallet: it pairs from the dApp's link, approves an EVM and a
Polkadot account, and answers each request with the app's own request
preparation and signer, as the confirmation does after the PIN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/walletconnect-protocol.md now says how the app pairs (wc: codes and
links, fearless://wc?uri=), what it signs and checks before the
confirmation, how the requesting site is shown, the PIN step, the
per-network remote vetoes, and the live signing check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's request and proposal ids are Int64 (Reown's were RPCID and
String), and its JSONRPCError is FearlessWalletConnect.JSONRPCError,
qualified because SSFUtils has a type of the same name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live signing check found that the polkadot_signMessage and
polkadot_signTransaction answers carried a random 64-bit id. dApps read
the answer in JavaScript, where an integer beyond 2^53 becomes a BigInt
that plain JSON.stringify cannot write. The id is now between 1 and
2^53 - 1.

The reference dApp scripts' in-memory storage writes such values too, and
the live signing test fails, instead of skipping, when the dApp never
writes its result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SwiftFormat's unusedArguments rule; no change in behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…quests

Security review of c82dd63, findings 3, 4 and 6 (client side):
- pair(uri:) refuses a link whose topic is a live session's, or a pairing's with
  another key (ClientError.topicInUse): its key would replace theirs. Opening the
  same pending link again changes nothing.
- respond(topic:requestId:response:) answers only a request the dApp still waits
  for (ClientError.requestNotFound otherwise); isPending(topic:requestId:) tells
  the app, expiry included, before the sweep runs.
- disconnectAll() ends every session and pairing for logout, and keeps going
  when one fails.

Package tests: 52 run, 0 failures, 2 env-gated skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review of c82dd63:
1. HIGH, app lock: the idle and background lock puts its PIN on the app's own
   window (SceneWindowFinder.mainWindow), never on the key request window that
   hides while locked, so no flow ending can dismiss it. A request sheet hidden
   with its window is not rejected; the main window takes key status back.
2. MEDIUM, blind signing: undecoded call data gets a warning, the contract, the
   selector and the raw data; transfer, transferFrom, approve, increaseAllowance
   (maximum = unlimited) and setApprovalForAll are decoded strictly, with a
   warning for trailing bytes. Typed data shows permits' spender, amount and
   deadline (DAI allowed: true = unlimited) and every signed field of anything
   else, with a warning. Polkadot calls other than transfers show the call and
   its arguments, Utility.batch* and Proxy.* inner calls numbered, with a warning.
3. MEDIUM, forget wallet: every session that granted one of the wallet's
   accounts ends (any namespace and chain, by account id); logout ends every
   session and pairing.
5. LOW: typed data whose domain carries an undeclared chainId is declined; a
   domain without one gets a warning; the hash comment is fixed.
6. LOW: expired or ended requests and proposals leave the queue or close with a
   notice; the confirmation checks the request is still pending before the PIN
   and before signing; a transaction already sent still shows its hash.
7. LOW: at most 10 flows wait (more are declined at once), at most 3 links wait
   for the unlock, and requests are keyed by topic and id.
8. LOW: Polkadot chains match only the exact 32-character CAIP-13 reference.
(4 and the client half of 3 and 6 are in the previous commit.)

Focused hosted tests: 361 run, 0 failures, 2 env-gated skips; the lock, gate and
lifecycle classes 5 times over: 240 runs, 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
takemiyamakoto and others added 17 commits November 8, 2026 08:23
Re-review of 4619160, problem C: disconnectAll() forgot one topic, then
waited for that peer, before the next. It now forgets every session and pairing
locally in one write first (loading the record when the client has not started,
and removing earlier SDKs' records of them), then tells every peer at once,
best effort. Nothing comes back on a later start.

Package tests: 54 run, 0 failures, 2 env-gated skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review of 4619160:
A. HIGH: permits are read only when the primary type is defined exactly as
   EIP-2612, DAI or Permit2 define it (compared as the EIP-712 type string), and
   only from declared fields, so undeclared keys such as allowed:false or
   expiry:0 can't change what is shown. The token and name come from the domain
   only when EIP712Domain declares them. Every signed field is listed under the
   permit too. Undeclared keys get a warning that they are not signed. A value
   whose shape differs from its declared type is declined.
B. MEDIUM: nothing is cut. Typed data over 500 fields or 10 levels, and a
   Substrate call carrying over 100 calls or past 60,000 characters or 24
   levels, is declined before the confirmation opens. Substrate calls get a
   summary of every call they carry, by call and account with counts and totals:
   fund-moving and rarer calls first, then larger amounts. Lists of numbers
   (bytes) show on one line.
C. LOW: logout reaches WalletConnect's record even before it was set up.
D. LOW: at most 5 requests per session, 5 proposals and 20 flows in all.
E. LOW: transfer, transferFrom and approve on a contract that isn't a known
   token show "Amount or item ID".

Focused hosted tests: 383 run, 0 failures, 2 env-gated skips; lock, gate and
lifecycle classes 5 times over: 255 runs, 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tocol notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Third review of c1201eb, F: the ABI encoder that hashes typed data reads a
number string as decimal, then as unprefixed hex, then as signed, and encodes
negatives in two's complement, so "-1" or -1 for a uint256 signed as 2^256-1,
"ff" as 255 and "1e18" as 0x1e18, while the screen showed the text as written.
A malformed address was also hashed as other bytes.

Before anything is hashed or shown, every value of an atomic declared type, in
the domain and at any depth of the message, is now parsed strictly and written
back in one canonical form (WalletConnectTypedDataValues):
- uintN/intN: decimal without leading zeros, 0x hex, or a JSON integer, within
  the bit width; negatives only for intN; shown and hashed as decimal.
- address: 0x and 40 hex digits. bool: a JSON bool, or 1 and 0.
- bytes: even-length 0x hex. bytesN: at most N bytes, right-padded as hashed.
- string: a JSON string. Fixed-size arrays must have their size.
Anything else is declined. The hash reads only the canonical data, so the
signature covers exactly what the confirmation shows. A hex domain chainId is
now read as its number and bound to that network.

Focused hosted tests: 390 run, 0 failures, 2 env-gated skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026.11.7 is in App Review and counts as consumed; 2026.11.8 replaces it with
WalletConnect back through the in-repo client: wc: pairing and EVM and
Polkadot signing behind the PIN, remote vetoes and the requesting site,
after three independent security review passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply shared native lighting, raised surfaces, inset controls, approved brand assets, and informative wallet motion. Preserve accessible layouts and keep sheet rims static across recovery and signing contexts. Add regressions for motion policy, sync state, localized layouts, transaction states, and generated asset integrity.
The Profile authorization fixture called the data source's cellForRowAt
directly on every open(). That dequeues outside a table request, and a
second open() raises "Attempted to dequeue multiple cells for the same
index path". The raised exception unwinds through the async test, which
aborts the test host. The sun-lit row spacing exposed this: with tighter
footers, scrolling no longer recycled the rows between the two opens.

Scroll each row into place and read it with cellForRow(at:), the same
way UIKit asks for it. Test code only; the app's data source is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner, 8 November 2026: "on pages like this, we are not constrained to this
little circle. make the wings logo span most of the width of the screen".

PIN, welcome, onboarding start, About and the launch screens now show the
approved mark at full span: the badge's own wing, joint and disc paths without
the circle, at about 90% of the screen width and no taller than the circle was.
The generator embeds the master path text unchanged, so the wings stay
byte-identical. FWWingsView adds the sun's glow and drops it under reduced
effects and high contrast. The wallet's hero keeps the round sun because its
sync arc needs the circle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Screens from the final build showed two flaws. The PIN page's scroll view cut
the glow in a straight line above the wings, so the glow now fades within the
room every page keeps above the mark. The About list still had the system's
black cells and a black header fade under the lit page; it now shows the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the owner-verified PIN keypad sizing from TestFlight 2026.11.9: 72 pt
keys, comfortable spacing and vertical centering. The release line's metrics
decide the keypad's sizes; the sun-lit paint and the wide wings sit on top. Both
sides' PIN tests are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dismissed authorization releases Profile a run-loop turn after UIKit ends the
transition, so a tap on the very next poll could be ignored and fail the test.
The fixture now waits for that release before tapping. A release that never
comes still fails, so the test still guards against a stuck Change PIN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@takemiyamakoto
takemiyamakoto requested review from a team as code owners November 8, 2026 19:47
nvzhu
nvzhu previously approved these changes Nov 8, 2026
@takemiyamakoto

Copy link
Copy Markdown
Contributor Author

Folded into #1326, which brings every open iOS PR into master in one change, so one code-owner approval covers it. This PR's branch is kept.

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.

2 participants