Skip to content

Release v3.9.18 - #194

Merged
kcw-grunt merged 219 commits into
mainfrom
release/v3.9.18
Aug 25, 2026
Merged

Release v3.9.18#194
kcw-grunt merged 219 commits into
mainfrom
release/v3.9.18

Conversation

@grunt-claude-bot

@grunt-claude-bot grunt-claude-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

What's Changed

🐛 Bug Fixes

✨ UI/UX

  • Reworked the Buy/Receive sheet (BuyReceiveView) to match the Android app's layout: explicit close button, full (non-truncated) receive address with a "Copy New Address" affordance, a currency picker that shows more surrounding rows, bordered preset-amount chips (min/10x/max/Custom) with inline custom entry, a unified "Buy LTC" button, and a translucent sheet background (Relayout BuyReceiveView to match Android BUY/RECEIVE sheet #177)

🌐 Localization

  • Removed 22 dead Localizable.xcstrings entries with no remaining source reference
  • Added the missing translations (21 locales each) for BUY / RECEIVE, COPY NEW ADDRESS, and POWERED BY MOONPAY, which had been silently falling back to English for every non-English user

🧹 Housekeeping

Full Changelog: v3.9.15...v3.9.18

Testing

  • plutil -lint on the updated project.pbxproj passes.

🤖 Generated with Claude Code

grunt-claude-bot and others added 25 commits August 13, 2026 20:51
… BWIOSGdx link (#156)

* Update project

* update game to v1.6.7(8)

* update the v1.6.8

* Fix Simulator crash: remove BWIOSGdx.xcframework re-added to Frameworks/Embed

014f068 ("update the v1.6.8") re-added BWIOSGdx.xcframework to the
unconditional Frameworks and Embed Frameworks build phases -- almost
certainly via Xcode's own "add framework" UI while troubleshooting a
device build, since that's Xcode's default behavior for any framework
added through its interface.

This undoes ios#154's device-only-scoped linking fix (OTHER_LDFLAGS/
FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*], deliberately NOT using the
Frameworks/Embed Frameworks phases, since phase membership doesn't
respect SDK qualifiers and applies to every platform unconditionally).
Harmless on device (just a redundant second link path alongside the
scoped one), but reintroduces the exact Simulator crash that fix
eliminated: Simulator tries to link/embed BWIOSGdx.xcframework, whose
simulator slice has device-only vendored native libs
(gdx-box2d/gdx-freetype/gdx-miniaudio/gdx core -- byte-identical to
the device slice, confirmed earlier this session).

Verified: reproduced against a clean `develop` in an isolated
worktree first (fresh clone, fresh submodules, no local changes) to
confirm this wasn't actually a develop regression -- builds, installs,
and launches fine there, proving the bug was local-only. Then applied
the same fix here: BUILD SUCCEEDED, installs, launches, and stays
running on Simulator.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* bumped

---------

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Removes code and resources confirmed unused across the ios repo
(submodules excluded), validated with a full clean build + test
build after every batch:

- Whole orphaned directories not referenced by the Xcode project:
  brainwallet/Launch Resources/ (stale duplicate of PreLaunchResources/)
  and brainwalletTests/ (leftover from a retired test target).
- 55 orphaned .swift files never wired into any build target, mostly
  stale duplicates left behind by the SwiftUI rewrite (e.g. root-level
  ApplicationController.swift, BuyViewModel.swift,
  TabBarViewController.swift), plus a fully commented-out
  UtilityTests.swift.
- Unused code inside otherwise-live files: dead Redux actions in
  Actions.swift (and their now-orphaned clone helpers), an unused
  GradientView/GradientDrawable gradient-drawing path,
  PaymentProtocolACK/PaymentProtocolPayment, several unused SwiftUI
  view modifiers/structs, and a few commented-out dead-code blocks.
- Dead resources: 3 unused product images (amazon_logo, je_logo,
  visa_logo), 3 sound files bundled but never played (clicksound.wav,
  clicksound.aiff, clickseedword.mp3), and a stray committed
  testScript Mach-O binary.

project.pbxproj updated to match everywhere a compiled file or
resource reference was removed.

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Checkpoint build covering everything merged into develop over the
last 48 hours (2026-08-13 through 2026-08-14):

- #153 Fix Simulator launch crash + flaky LockScreenViewUITests:
  link BWIOSGdx.xcframework device-only (via OTHER_LDFLAGS/
  FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*] instead of the unconditional
  Frameworks build phase) so Simulator builds never require its
  device-only vendored natives; also stabilized a flaky lock-screen
  UI test that depended on locale/theme-dependent SF Symbol labels.

- #154 Fix CircleCI + Xcode Cloud build failures: stopped trying to
  build BWIOSGdx.xcframework in CI (Simulator never needs it) and
  moved its embedding into the existing Flatten+Sign+Relocate script;
  fixed ci_post_clone.sh to actually dereference and base64-decode
  its secret env vars into GoogleService-Info.plist /
  remote-config-defaults.plist / service-data.plist /
  Brainwallet-StoreKit-v1.storekit; added fail-fast validation for
  all four generated files across both CI providers; fixed a
  locale- and timezone-dependent date-formatter test failure; and
  removed a GoogleService-Info.plist content dump from three CircleCI
  configs that could have leaked API keys in this public repo's logs.

- #155 Bump Modules/core: pulls in a fix for a Crashlytics-reported
  EXC_BAD_ACCESS in _peerThreadRoutine (null-checks threadCleanup
  before invoking it, matching every other optional callback in the
  same function).

- #156 Bump bw-gdlib to v1.6.8, fix Simulator crash regression:
  the game-SDK bump's Xcode-UI framework changes re-added
  BWIOSGdx.xcframework to the unconditional Frameworks/Embed phases,
  regressing #153's Simulator fix; removed it from those phases again.

- #157 chore: remove defunct code, resources, and dead files: 328
  files / ~12.5k lines removed across the ios repo (orphaned Launch
  Resources/ and brainwalletTests/ directories, 55 files never wired
  into any build target, dead code cascades inside otherwise-live
  files, and unused images/sounds), validated with a full clean build
  and test-build after every batch.

This commit itself: build-number bump only, plus Xcode's own
formatting pass over Localizable.xcstrings (key/value spacing) picked
up incidentally when the project was last opened.

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
…o-98%-sync (#161)

* fix: sync-duration analytics event never fired; log foreground time-to-98%-sync

user_did_complete_sync required walletState.syncProgress >= 0.999 AND
walletState.syncState == .syncing in the same dispatched state, but
WalletCoordinator.onSyncStop() invalidates the progress timer and
dispatches setSyncingState(.success) as a separate store action from
the final setProgress. By the time the last block-height update
landed, syncState had usually already flipped to .success, so the
isSyncing gate skipped the check entirely -- the event essentially
never fired.

Goal is a foreground-sync-duration average, to be able to promote the
shortest sync time to users -- so this measures accumulated active
syncing time while the app was in the foreground, not wall-clock time
from wallet creation to completion (which would include however long
the app sat backgrounded/closed mid-sync, and isn't a fair "how fast
is our sync" number).

- WalletCoordinator now owns the metric, since it's the actual owner
  of the sync start/stop lifecycle (not a Redux subscription racing
  against it). Tracks active-foreground-sync segments: a segment opens
  on onSyncStart or didBecomeActiveNotification (only if still mid-sync),
  and closes on onSyncStop or willResignActiveNotification -- so the
  ~30s grace period the peer manager can keep running into the
  background (until the background task's expiration handler
  disconnects it) isn't counted as foreground time either. Elapsed
  segments accumulate into UserDefaults.foregroundSyncDurationSeconds.
  Once block-height progress first crosses kSyncDurationThreshold
  (98%), logs the accumulated total in seconds as a Firebase parameter:
  user_did_complete_sync now sends sync_duration_seconds instead of
  parameters: nil.
- Both foregroundSyncDurationSeconds and the one-shot
  hasLoggedInitialSyncDuration flag are persisted (a sync can span
  multiple app sessions before reaching 98%), and both get wiped for
  free by the existing wipeWallet() (removePersistentDomain), so a
  fresh wallet gets a fresh measurement window with no extra cleanup.
- Outlier cap: durations past kMaxSyncDurationSeconds (24h) are
  discarded rather than logged. Matters less now that background time
  no longer leaks into the number in the first place, but kept as a
  backstop against clock skew / a pathologically bad connection.
- NewMainViewModel.didRestoreOldBrainwallet(): dispatch
  WalletChange.setWalletCreationDate(Date()), matching
  generateNewWallet() -- Restore never set this, so walletState.creationDate
  (persisted into WalletInfo via KVStoreCoordinator) stayed at
  Date.zeroValue() for every restored wallet. Independent fix, kept
  even though the duration metric no longer depends on creationDate.

Verified via xcodebuild build and build-for-testing (both succeed).

Still needed: register sync_duration_seconds as a Custom Definition
in the Firebase console (Analytics > Custom definitions) to use it in
Reporting -- that's a console-side step, not code. p50/p90 aren't
something the client can compute (each wallet only ever sends one
value); they require the BigQuery export linked and a percentile query
over sync_duration_seconds, not a code change.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: FallinScene/WelcomeFallinScene leak forever via unweakified recursive delay (#162)

FallinScene.makeDot() reschedules itself via delay(2.0) { ... self.makeDot() ... }
with no [weak self], no cancellation, and no termination condition. Every
call -- including the automatic one from didMove(to:) -- starts a GCD
timer chain on the main queue that fires every ~2s for the rest of the
process's life; the closure's strong self-capture means the scene can
never be deallocated once it starts. This isn't test-only: FallinScene
is live via FallinMojiDemoView -> GameHubBentoView, so every time a
user opens the Game Hub bento, this leaks a scene that recurses forever
in the background.

Root-caused BrainwalletUnitTests failure:
  FallinSceneTests.testMakeDot_CreatesLabelNode()
  Asynchronous wait failed: Exceeded timeout of 1 seconds, with
  unfulfilled expectations: "Wait for node creation"
scene.makeDot() itself is synchronous (addChild happens immediately);
the wait is just scaffolding. The 6+ tests in FallinSceneTests each
call makeDot(), leaving behind permanently-recursing orphaned scenes
that pile up competing for the main run loop across the suite -- enough
backlog to make a trivial 0.1s asyncAfter miss a 1s timeout.

Fix: [weak self] + guard in the recursive closure, so an orphaned scene
can actually be deallocated once nothing else references it, stopping
the chain.

WelcomeFallinScene.makeSprites() has the same pattern (delay(1.0) { ...
self.makeSprites() ... }, no [weak self]). It does self-terminate (the
30s countdown reaching 0 breaks the chain), but without weakifying, a
user backing out of the view mid-game keeps it recursing and mutating
@binding var counter/countdown/didStartGame for up to 30s after the
view is gone. Fixed the same way, plus the same missing [weak self] in
touchesBegan's one-shot delay(0.1) (smaller window, same pattern).

Verified: ran FallinSceneTests directly on iPhone 16 Pro (iOS 18.5) --
all 8 tests pass, including testMakeDot_CreatesLabelNode() at 0.107s.
xcodebuild build and build-for-testing both succeed.

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>

---------

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Bumps Private/bw-gdlib from ab90257 to 68b03cb:
- Add QR privacy hide/show toggle to GameEndView
- Bump fallinmoji version to 1.6.9 (code 10)
- Update README release notes and badge for v1.6.9

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <noreply@anthropic.com>
Co-authored-by: kcw-grunt <kerry@grunt.ltd>
* Update project.pbxproj

* chore: bump build to 2602307

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

---------

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
* chore: remove CircleCI, rely on Xcode Cloud for CI

CircleCI's macOS executor costs had gotten too high. Xcode Cloud
(ci_scripts/) already runs the build/test pipeline, so it's now the
sole CI for this repo.

- Delete .circleci/ (per-push unit tests, daily-develop maintenance
  job, and UI-test/weekly-maintenance job)
- Remove setup_circle_ci from fastlane/Fastfile's before_all
- Move the Linux-only i18n translation-coverage check
  (scripts/test_i18n_coverage.py) to a new GitHub Actions workflow
  (.github/workflows/i18n-coverage.yml) since it has no Xcode Cloud
  equivalent
- Drop the CircleCI status badge and the CircleCI-updated Gist
  tests-badge from README; update the Testing section to reference
  Xcode Cloud + the new i18n workflow
- Clean up stale .circleci references in ci_scripts/ci_post_clone.sh
  and .github/workflows/pr-summary.yml

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* remove duplicate bwiosgdx

* bumped code

---------

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
* fix: clear easy Swift compiler warnings

Mechanical, behavior-preserving cleanup of the low-risk warnings from a
full warnings review: unused GeometryReader width/height locals, a dead
if-let block, unnecessary try? on a non-throwing call, a non-exhaustive
switch on UIGestureRecognizer.State, deprecated Data(bytes:) inits, a
write-only dead variable, and refactoring WalletCoordinator's manual
weak-var-capture pattern to the idiomatic [weak self] closure capture.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: replace deprecated UIApplication.windows with currentKeyWindow helper

UIApplication.windows was deprecated in iOS 15 in favor of going through
UIWindowScene. Adds a single UIApplication.currentKeyWindow extension
(via UIWindowScene.keyWindow across connectedScenes) and swaps in the
three call sites that were each independently doing the old
`.windows.filter { $0.isKeyWindow }.first` dance -- ModalPresenter.swift,
ModalPresenter+Extension.swift, and ModalTransitionDelegate.swift.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: replace deprecated SwiftUI .animation(_:) with value-scoped variant

.animation(_:) (no value:) implicitly animates any state change to the
view, which is why it was deprecated in iOS 15. Scope it to the specific
binding actually driving the change (shouldExpandBlockchain) instead.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: migrate SCNetworkReachability to NWPathMonitor

SCNetworkReachabilityCreateWithName/SetCallback/SetDispatchQueue/GetFlags
were deprecated in iOS 17.4. Rewrites ReachabilityMonitor on top of
Network.framework's NWPathMonitor, keeping its existing public surface
(didChange closure, isReachable) so its three call sites (WalletCoordinator,
ModalPresenter, ApplicationController) are unaffected. Also drops the
manual Unmanaged/UnsafeMutableRawPointer C-callback trampoline the old
implementation needed.

WalletManager.networkIsReachable() -- called synchronously from the C
peer-manager's networkIsReachable callback, so it can't become async --
now owns a long-lived ReachabilityMonitor and just reads its isReachable
property instead of the deprecated SCNetworkReachabilityCreateWithAddress/
GetFlags pair.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: scope dangling raw-pointer usage in BRAddress and seed generation

UnsafeMutableRawPointer(mutating: &x) / UnsafeRawPointer([x]) are only
valid for the duration of the single call they're constructed in;
chaining .assumingMemoryBound(...) and using the result afterward (or,
worse, across several later calls) uses the pointer outside that
guaranteed scope -- undefined behavior the compiler now flags as
"results in a dangling pointer".

BRAddressExtension.swift: every init/accessor touching the address's raw
`s` buffer (init?(string:), init?(scriptPubKey:), init?(scriptSig:),
scriptPubKey, hash160, description) now does its C-interop call from
inside a withUnsafeBytes/withUnsafeMutableBytes(of:) closure instead of
a pointer built ahead of time.

WalletManager+Auth.swift: setRandomSeedPhrase()'s BIP39 entropy pointer
(entropyRef) was reused across SecRandomCopyBytes and two BRBIP39Encode
calls spanning several lines -- the most serious instance of this
pattern, since it's seed-entropy generation. Wrapped all three uses
inside a single withUnsafeMutableBytes(of: &entropy) closure.

Verified via BRAddressTests, WalletCreationTests, and
WalletAuthenticationTests (all pass) that behavior is unchanged.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* test: add unit tests for BRAddressExtension and WalletManager+Auth

BRAddressExtensionTests: self-contained, deterministic coverage of
BRAddress's init(string:)/init(scriptPubKey:)/scriptPubKey/hash160/
description/Equatable/Hashable, using a hand-built P2PKH scriptPubKey
fixture rather than depending on wallet setup or the keychain.

WalletManagerAuthTests: covers setSeedPhrase, the seedPhrase(pin:) round
trip, changePin, pinLength, and wipeWallet -- business logic not already
exercised by WalletAuthenticationTests (pin lock-out) or WalletCreationTests
(random seed-phrase generation).

Both exercise the exact code paths touched by the preceding dangling-
pointer and SCNetworkReachability fixes, so they'll catch regressions
there going forward.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: migrate withUnsafeBytes/withUnsafeMutableBytes off deprecated typed-pointer overloads

Data.withUnsafeBytes/withUnsafeMutableBytes closures typed with a
concrete pointer type (e.g. (UnsafePointer<UInt8>) -> R) resolve to the
deprecated overload; the replacement overload's closure parameter is
UnsafeRawBufferPointer/UnsafeMutableRawBufferPointer. Migrated every
site across Extensions.swift (md5, base58 encode/decode, sha1/sha256,
uInt256, the uInt8/32/64(atOffset:) accessors, compactSign, genNonce,
the masterPubKey getter, BRKey.publicKey), BRKeyExtension.swift
(privKey/bip38Key), BRReplicatedKVStore.swift's genNonce duplicate, and
WalletManager+Auth.swift (creationTime load, BIP39 encode, apiAuthKey,
the generic Int64 keychainItem loader).

Where the underlying C function takes void* (BRMD5, BRSHA1/256,
BRKeyCompactSign, BRKeyPubKey) the fix is just .baseAddress; where it
takes a typed pointer (BRBase58Encode/Decode's char*/uint8_t*,
BRKeyPrivKey/BRKeyBIP38Key's char*) it's
.baseAddress?.assumingMemoryBound(to:); reinterpreting raw bytes as a
value (uInt256, the offset accessors, the Int64 keychain case) uses
.load(as:).

BRMasterPubKey.pubKey is a 33-byte C-array-as-tuple; calling the global
withUnsafeMutableBytes(of:) on that tuple projection crashed the type
checker ("failed to produce diagnostic for expression") instead of
compiling, so the masterPubKey getter instead uses
withUnsafeMutablePointer(to:) + withMemoryRebound(to: UInt8.self).

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* test: add ExtensionsCryptoTests for the Extensions.swift crypto/encoding helpers

md5, sha1/sha256, base58 encode/decode, uInt256, the uInt8/32/64(atOffset:)
accessors, and the BRMasterPubKey<->Data round trip had no test coverage
at all. Added known-vector and round-trip tests for each, verified
against the withUnsafeBytes/withUnsafeMutableBytes migration in the
preceding commit -- a mistake in that pointer plumbing would show up
here as a wrong hash rather than just a cleared warning.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: clear remaining deprecation warnings, badge count, and mock test scaffolding

- applicationIconBadgeNumber (deprecated iOS 17): WalletCoordinator now tracks
  its own pending badge count (UserDefaults.pendingNotificationBadgeCount,
  reset alongside AppDelegate's existing setBadgeCount(0) calls) since
  UNUserNotificationCenter.setBadgeCount(_:) has no matching getter.
- FakeAuthenticator/testPublicKeyEncoding (BWAPIClientTests.swift): migrated
  the remaining withUnsafeBytes/withUnsafeMutableBytes calls off the
  deprecated typed-pointer overloads; apiAuthKey's BRKeyPrivKey call also
  had the pointer-escapes-the-closure pattern fixed (matches the
  BRKey.publicKey fix from earlier in this cleanup).
- MockURLSessionDataTask / MockURLSession: restated `@unchecked Sendable`
  (required under Swift 6 concurrency checking when subclassing a Sendable
  Foundation type) with comments explaining why each is safe.
- Removed MockURLSession.swift and MockURLSessionDataTask.swift entirely --
  neither was ever actually instantiated (mockURLSession stayed nil), so
  the URLSessionDataTask() deprecation inside them was dead code; deleting
  it is the only real fix, since wrapping the call in a helper only
  relocates the same warning rather than removing it (verified).
- Replaced the two remaining bare URLSessionDataTask() calls in
  BWAPIClientTests.swift's redirect/challenge tests with the real
  URLSession.dataTask(with:) factory method.

That last change gave testURLSession_Redirect_ToDifferentHost_DoesNotFollow
a real task for the first time, which exposed it as a false positive: it
only passed because a never-started bare task has a nil currentRequest,
not because the code under test actually blocked the redirect.
BWAPIClient.urlSession(_:task:willPerformHTTPRedirection:newRequest:...)
only checked the redirect's *origin* host against api.grunt.ltd, never the
*destination* -- so a redirect issued by our own API pointing anywhere
else would have been followed. Fixed both:
- BWAPIClient.swift now also requires the redirect's destination host/scheme
  to match our own API before following it.
- Fixed the existing test to genuinely exercise that check, and added
  testURLSession_Redirect_FromDifferentHost_DoesNotFollow (untrusted
  origin) and testURLSession_Redirect_SameHost_Follows (positive case)
  alongside it.

Also included: SignupAskViewTests' two branch-simplification fixes,
MockAuthenticationChallengeSender split into its own file, and removing
the long-dead "Run Script for CircleCI SPM Caching" build phase left over
from the earlier CircleCI removal.

Verified with a full clean build (0 errors, 0 warnings in every touched
file) and the full BWAPIClientTests/SignupAskViewTests suites (33 tests).

Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>

---------

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
…g the way (#175)

* fix: remove dead code found while auditing warnings (UIView frame swizzle, MessageUIPresenter)

UIView+FrameChangeBlocking.swift: swizzled UIView's frame setter app-wide
at launch, gating writes on a per-instance isFrameChangeBlocked flag --
but nothing anywhere ever set that flag to true, so the swizzle had zero
functional effect; every frame write just paid for an extra method-call
hop through requestSetFrame(_:) for nothing. Its "equivalent of
dispatch_once" comment was also stale/inaccurate: a bare static func has
no actual once-guard, so a second call to swizzleSetFrame() would have
silently un-swizzled it via method_exchangeImplementations.

MessageUIPresenter: instantiated once in ModalPresenter (messagePresenter),
but that property was never referenced again anywhere -- none of
presentMailCompose/presentFeedbackCompose/presentSupportCompose were ever
called.

Removed both entirely, including their pbxproj file/build-phase entries.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: stop performRequestWithRetry from recursing unconditionally

attemptRequest() called itself immediately after firing its URLSession
data task, outside the retry-on-failure branch -- so every call recursed
synchronously before any network response could return, regardless of
whether the request actually needed retrying. This was flagged during an
earlier warnings review (the compiler's "function call causes an infinite
recursion" diagnostic on this same function) as a critical bug distinct
from the review's warning cleanup; fixing it here.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: small Swift-safety cleanups in Legacy_BW_BRClasses

- BRMasterKeyExtension.swift: mark the retroactive Equatable conformance
  on BRMasterPubKey (an imported BRCore type) with @retroactive, per the
  compiler warning that this won't behave correctly if BRCore itself adds
  the conformance in the future.
- BRReplicatedKVStore.swift: replace a force-cast (as!) with a safe
  optional cast (as?) when handing a caught error to completionHandler.
- BRTxInputExtension.swift: line-wrap reformatting only, no behavior change.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: scope dangling pointers and migrate deprecated Security APIs in PaymentProtocolRequest

certs/digest: UnsafeMutablePointer(mutating:) on a Swift array/let escaped
its guaranteed-valid scope (same pattern fixed elsewhere this cleanup);
now scoped via withUnsafeMutableBufferPointer.

isValid()'s certificate-trust and signature-verification flow:
- SecTrustEvaluate + SecTrustCopyProperties (manual error-string
  extraction from a loosely-typed properties array) -> SecTrustEvaluateWithError,
  which returns the trust decision and a descriptive CFError in one call.
- SecTrustCopyPublicKey -> SecTrustCopyKey (direct rename).
- SecKeyRawVerify(_, .PKCS1SHA256/.PKCS1SHA1, ...) -> SecKeyVerifySignature(_,
  .rsaSignatureDigestPKCS1v15SHA256/SHA1, ...), the documented replacement
  for verifying a PKCS1v15 signature over an already-hashed digest.

The errSecUnimplemented-sentinel trick for distinguishing "unsupported
pkiType" from "verification failed" is now an explicit pkiType check
instead of relying on an OSStatus that was never reassigned.

Verified via full clean build (0 errors/warnings in this file) and the
existing PaymentRequestTests suite. isValid()'s trust/signature path
itself has no direct automated coverage (pre-existing gap, not
introduced here) -- building real fixtures for it would need a
self-signed cert and BRTxOutput construction, worth its own task.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

* fix: migrate userAccount keychain storage off deprecated NSKeyedArchiver APIs

The generic keychainItem<T>/setKeychainItem<T> pair's [AnyHashable: Any]
case (backing WalletManager.userAccount) used the deprecated
NSKeyedArchiver.archivedData(withRootObject:) / NSKeyedUnarchiver.unarchiveObject(with:).
Both functions already throw, so:
- write: archivedData(withRootObject:requiringSecureCoding: true)
- read: unarchivedObject(ofClasses:from:) with an explicit allowlist of
  the plist-compatible classes a userAccount blob can actually contain
  (NSDictionary/NSArray/NSString/NSNumber/NSDate/NSData/NSNull)

userAccount had no test coverage at all; added a round-trip test and a
nil-before-set test to WalletManagerAuthTests.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>

---------

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
- Header now shows an explicit close button alongside centered title
- QR/address block shows the full address (no truncation), centered
  "COPY NEW ADDRESS" affordance with a bordered copy icon
- Currency wheel picker shows more surrounding rows, has its default
  row background stripped (UIPickerView+Extension.swift), and brackets
  the selected row with hairlines
- Preset amount segmented control replaced with bordered pill chips
  (min / 10x / max / Custom), Custom reveals inline amount entry
- Buy button unified into a single lavender rounded rect with the
  MoonPay attribution folded in, relabeled BUY LTC
- All content now sits in a single BrainwalletColor.surface container
  sized 90% width / 60%[->75%] height of the sheet
- Sheet itself now presented as translucent (presentationBackground +
  a dialed-down VariableBlurView, since ultraThinMaterial + opacity
  only fades the blur rather than thinning it)

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
"GET LTC" and "Set amount:" no longer appear anywhere in source
(BuyReceiveView relayout renamed the buy button to "BUY LTC" and
replaced the standalone "Set amount:" row with the preset chips /
inline Custom entry), so their catalog entries were dead weight.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
json.dump's default separators omit the space before each colon that
Xcode's String Catalog serializer always writes ("key" : value vs
"key": value). That mismatch meant every auto-translate run rewrote
every line of the ~37k-line file, even when only a couple of strings
actually changed — producing unreviewable, unmergeable PRs and
spawning a fresh one on every push that touched the file (#159, #160,
#164, #165, #167-170, #178, #180, #182, #183, #185, ...).

Verified the fix is idempotent: round-tripping the current
Localizable.xcstrings through load/save now produces a byte-identical
file.

Co-Authored-By: grunt-claude-bot <noreply@grunt.ltd>
Co-Authored-By: kcw-grunt <kerry@grunt.ltd>
…zable-keys

chore: remove unused Localizable.xcstrings keys
fix: match Xcode's JSON formatting when writing Localizable.xcstrings
Cherry-picked from release/v3.9.17, where this string had full
translations but was empty on develop.

Co-Authored-By: grunt-claude-bot <noreply@grunt.ltd>
Co-Authored-By: kcw-grunt <kerry@grunt.ltd>
Update game and restore COPY NEW ADDRESS translations
)

Dropped the pull_request trigger. It re-fired the workflow on every
commit pushed to any PR touching Localizable.xcstrings — including
long-lived release branches (4 translation PRs spawned against
release/v3.9.16-into-main in one afternoon) and the bot's own PR
check runs (#188, opened against the '184/merge' ref while #184 was
still under review, duplicating #189's content once #184 actually
merged).

Translations only need generating once a change actually lands on
main/develop, not during PR review, so push-only is sufficient and
removes the self-duplicating behavior. Also drops the now-dead
github.event.pull_request.* fallbacks that existed only to support
that trigger.

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <noreply@grunt.ltd>
Co-authored-by: kcw-grunt <kerry@grunt.ltd>
Languages: ar, de, es-419, fa-IR, fr, hi, id, it, ja, ko, pa, pl, pt-BR, nl, ru, sv, th, tr, uk, zh-Hans, zh-Hant
Triggered by: 7fe008e

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
main only ever receives squash-merges (one commit per release), so
without a periodic back-merge like this, develop's linear history and
main's synthetic squash-commit chain never reconnect — every future
release PR's diff against main ends up showing the entire codebase
instead of just that release's own commits, because GitHub can't find
a common ancestor (404 'No common ancestor').

This redoes and updates a same-purpose merge (6a1c30b, 2026-08-15)
that was made locally but never pushed, brought up to main's current
tip (a9e7cee, Release v3.9.16 #166).

This is a pure -s ours merge: main's tip becomes a second parent for
ancestry purposes only, no file content changes. Merge this PR with
'Create a merge commit' — squashing would discard the second parent
and reintroduce the exact problem this fixes.

Co-Authored-By: grunt-claude-bot <noreply@grunt.ltd>
Co-Authored-By: kcw-grunt <kerry@grunt.ltd>
…-develop

chore: back-merge main into develop to keep shared ancestry
## Highlights since v3.9.16

- Reworked the Buy/Receive sheet (BuyReceiveView) to match the Android
  app's layout (#177)
- Cleared remaining deprecated Security/Foundation API usage and a real
  infinite-recursion bug (#175)
- Reduced the Swift compiler warnings list (#174)
- Removed CircleCI — Xcode Cloud is now the sole CI (#173)
- Updated Private/bw-gdlib to v1.6.9 (#171) and Modules/core (#176)
- Restored the missing translations for COPY NEW ADDRESS, and added
  translations for BUY / RECEIVE and POWERED BY MOONPAY (21 locales
  each) — these existed in an earlier release attempt but were never
  merged back into develop (#184)
- Removed 2 dead Localizable.xcstrings keys with no remaining source
  references: GET LTC, Set amount: (#179)
- Fixed the auto-translate script writing Localizable.xcstrings with
  JSON formatting that didn't match Xcode's String Catalog serializer,
  which was rewriting every line of the file on every run and
  producing unreviewable, unmergeable translation PRs (#186)
- Fixed the auto-translate workflow re-triggering on every push to any
  open PR (not just merges to main/develop), which was spawning
  duplicate translation PRs against long-lived branches (#190)
- Picked up 42 additional previously-missing translations across 21
  locales, generated cleanly after the above two fixes (#189)
- Bumped MARKETING_VERSION to 3.9.17

## Build
CURRENT_PROJECT_VERSION bumped 2602309 -> 2602311.

Co-Authored-By: grunt-claude-bot <noreply@grunt.ltd>
Co-Authored-By: kcw-grunt <kerry@grunt.ltd>
…eption crash

The global UIPickerView.layoutSubviews()/didMoveToWindow() override added in
PR #177 to strip the wheel currency picker's row background was applied to
every UIPickerView in the app (home screen, onboarding, settings, top-up,
buy/receive). Touching the picker's subviews that early forced UIKit to
initialize its internal per-component table views before SwiftUI's Picker(.wheel)
had finished wiring its data source, leaving the picker locked at 0 components.
When SwiftUI later called selectedRow(inComponent:) to sync the selection
binding, UIKit found no table for component 0 and aborted with
NSInternalInconsistencyException: "Tried to fetch selected row in component 0,
but there are only 0 tables."

This crashed 10 sessions / 1 user in 3.9.17 (builds 2602310, 2602311) per
Crashlytics, starting exactly when the extension shipped (Aug 19).

Reverts to the standard UIPickerView appearance (wheel pickers show their
default row background again) in exchange for removing the crash.

Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
@kcw-grunt
kcw-grunt merged commit 9460f6a into main Aug 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants