Release v3.9.18 - #194
Merged
Merged
Conversation
Compiled!!!
…elop # Conflicts: # .gitignore # LICENSE
- Removed nothing test
update references to strings - Readded BIP39 list update rough localization strings Updated the core library move references to critical files
- removed cruft - updated dark light mode for logo - removed extra image
… 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
approved these changes
Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's Changed
🐛 Bug Fixes
NSInternalInconsistencyException: Tried to fetch selected row in component 0, but there are only 0 tables— caused by a globalUIPickerViewbackground-clearing override that raced with SwiftUI's wheel picker setup during the unlock/onboarding transition; the override has been removed (Release v3.9.18 #194)user_did_complete_syncanalytics event never firing (fix: sync-duration analytics event never fired; log foreground time-to-98%-sync #161)LockScreenViewUITeststest (Fix Simulator launch crash + flaky LockScreenViewUITests #153)EXC_BAD_ACCESSin_peerThreadRoutinevia aModules/corebump (Fix Crashlytics crash: bump Modules/core (_peerThreadRoutine NULL threadCleanup) #155)Private/bw-gdlibto v1.6.8 (Bump bw-gdlib to v1.6.8, fix Simulator crash regression from re-added BWIOSGdx link #156)✨ UI/UX
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
Localizable.xcstringsentries with no remaining source referenceBUY / RECEIVE,COPY NEW ADDRESS, andPOWERED BY MOONPAY, which had been silently falling back to English for every non-English user🧹 Housekeeping
Private/bw-gdlibsubmodule to v1.6.9, adding a QR privacy hide/show toggle toGameEndView(chore: update bw-gdlib submodule to v1.6.9 #171)Modules/coresubmodule (Update core #176, Fix Crashlytics crash: bump Modules/core (_peerThreadRoutine NULL threadCleanup) #155).swiftfiles, dead Redux actions, unused SwiftUI view modifiers, and dead image/sound resources (328 files changed, 12,450 lines removed) (chore: remove defunct code, resources, and dead files #157)Full Changelog: v3.9.15...v3.9.18
Testing
plutil -linton the updatedproject.pbxprojpasses.🤖 Generated with Claude Code