Skip to content

🚀 [Release v3.9.17] - #193

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

🚀 [Release v3.9.17]#193
kcw-grunt merged 217 commits into
mainfrom
release/v3.9.17

Conversation

@grunt-claude-bot

Copy link
Copy Markdown
Collaborator

Release v3.9.17

Merge method

develop now has real shared ancestry with main (via #192), so this is a normal release PR — no bridge branch needed. Still merge with "Create a merge commit" rather than squash, so develop continues to fast-forward cleanly onto main for the next release.

Co-Authored-By: grunt-claude-bot noreply@grunt.ltd
Co-Authored-By: kcw-grunt kerry@grunt.ltd

🤖 Generated with Claude Code

grunt-claude-bot and others added 27 commits August 13, 2026 13:05
* Fix Simulator launch crash: link BWIOSGdx.xcframework device-only

Root cause: BWIOSGdx.xcframework was linked as a required dependency
for every platform via the Frameworks build phase, but its vendored
native libs (gdx-box2d/gdx-freetype/gdx-miniaudio/gdx core, from
libGDX's natives-ios artifacts) are device-only -- byte-identical
(and platform IOS, not IOSSIMULATOR) in both the xcframework's arm64
and arm64-simulator slice folders. The "Flatten+Sign+Relocate Asset
Frameworks" script stripped BWIOSGdx.framework from Simulator builds
to work around this, but that only deleted the file dyld was hard-
required to find, guaranteeing a launch-time crash:

  dyld: Library not loaded: @rpath/BWIOSGdx.framework/BWIOSGdx

Fix: remove BWIOSGdx.xcframework from the automatic Frameworks build
phase and link it explicitly device-only via
"OTHER_LDFLAGS[sdk=iphoneos*]" / "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]"
in both Debug and Release configs. Simulator builds now never
reference BWIOSGdx at the linker level, so there's nothing for dyld
to fail to find -- verified via otool -L (no BWIOSGdx load command)
and an end-to-end install+launch on Simulator with no crash reports.

The Swift-level #if !targetEnvironment(simulator) guards around
bwGameSDK usage in ApplicationController and GameContainerViewController
predate this commit and remain necessary since the game natives still
don't run on Simulator.

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

* Ignore ASC metadata skill credentials and .p8 keys

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

* Fix flaky LockScreenViewUITests: use stable accessibilityIdentifiers

testLockScreenView() was recorded against auto-generated accessibility
labels derived from SF Symbol images (.buttons["moon.stars"],
.buttons["sun.max"], .buttons["qrcode"]). Those labels are unstable:

- The theme button's icon/label flips between "moon.stars" and
  "sun.max" depending on userPrefersDarkMode, so a query for one or
  the other only matches if the button happens to be in that state
  when the test runs.
- iOS renders SF Symbol accessibility labels inconsistently across
  versions (e.g. "moon.stars" vs. the VoiceOver description "Clear
  Night"), which is why Xcode's recorder captured two alternate
  queries for the same element in the first place.

The theme button already had a stable .accessibilityIdentifier("Theme
Preference Button") that the test wasn't using. Added a matching
"QR Code Button" identifier to the receive-address button and updated
the test to key off both instead of the state/OS-dependent labels.

Verified via `xcodebuild build-for-testing` (compiles clean, matches
the real identifiers in LockScreenFooterView.swift). Could not verify
a passing run in this session: xcodebuild test-without-building fails
in this environment with an unrelated debugger-attach error
(DebuggerLLDB.DebuggerVersionStore.StoreError -> SpringBoard denies
launching the xctrunner with wait_for_debugger=1), reproducible even
with the sandbox lifted -- a local CLI/debugger-session issue, not a
code problem. Should pass when run from Xcode directly.

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>
* Fix CircleCI build failure: init and build the bw-gdlib xcframework

build_1841 (run_unit_tests_iPhone16ProMax) failed with:

  error: There is no XCFramework found at
  '/Users/distiller/ios/Private/bw-gdlib/ios/build/robovm/BWIOSGdx.xcframework'.
  (in target 'brainwallet' from project 'brainwallet')

Two things were missing:

1. The submodule init step only initialized Modules/core and
   Private/general-purpose -- Private/bw-gdlib was never checked out,
   so the directory didn't exist at all on the CI box.

2. Even with the submodule checked out, BWIOSGdx.xcframework still
   wouldn't exist: bw-gdlib's ios/build/ is gitignored (same as any
   other Gradle build output), because it's a RoboVM AOT-compiled
   artifact, not something checked into git. It has only ever been
   built manually on developers' Macs -- neither this repo's CI nor
   bw-gdlib's own CircleCI config (which runs on Linux and only does
   ./gradlew :core:test) has ever produced it.

Fix: init Private/bw-gdlib alongside the other submodules, and add a
build step that runs `./gradlew :ios:robovmInstall` (bw-gdlib's Gradle
task that produces build/robovm/BWIOSGdx.xcframework, confirmed via
`./gradlew :ios:tasks --all`) before the fastlane test run, installing
a JDK first if the macOS executor doesn't already have one.

Not yet verified against a live CircleCI run (no local runner
available) -- please confirm the next push on this branch turns the
job green, particularly the RoboVM build step's timing and the
openjdk@17 Homebrew install path on the macos.m1.medium.gen1 executor.

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

* Avoid needing BWIOSGdx.xcframework in CI at all

The previous commit tried to fix CI by building the framework there
(git submodule init + `./gradlew :ios:robovmInstall`), but that failed
too: bw-gdlib has its own nested submodule (android-build-logic, for
a Gradle version catalog) that a non-recursive `submodule update
--init Private/bw-gdlib` doesn't fetch, so Gradle failed on a missing
libs.versions.toml.

Chasing that down further, building the framework in CI was solving
the wrong problem. Simulator builds never need BWIOSGdx at all --
the game is entirely #if !targetEnvironment(simulator)-guarded in
Swift -- but Xcode's Frameworks and Embed Frameworks build phases
still referenced the xcframework unconditionally for every platform,
so Xcode tried to resolve/embed the file regardless and errored with
"There is no XCFramework found" the moment it didn't exist, before our
existing simulator-only linker scoping ([sdk=iphoneos*]) ever mattered.

Fix:
- Remove BWIOSGdx.xcframework from the Embed Frameworks build phase
  too (it was already removed from the Frameworks/link phase in the
  simulator-crash fix). Xcode now never touches the file for any
  platform.
- Move the embedding Xcode used to do automatically into the
  "Flatten+Sign+Relocate Asset Frameworks" script instead, gated the
  same way as the rest of that script: skip entirely on Simulator,
  copy+sign+flatten it for device builds (erroring with a clear
  message if the file's missing there, instead of Xcode's opaque one).
- Revert the CircleCI submodule-init and Gradle build step from the
  previous commit -- no longer needed since Simulator/CI builds don't
  touch BWIOSGdx at all now.

Verified locally: moved Private/bw-gdlib/ios/build/robovm/BWIOSGdx.xcframework
out of the way entirely (reproducing a fresh CI checkout) and did a
clean Simulator build -- BUILD SUCCEEDED, app installs and launches
normally. Restored the framework afterward and confirmed device-slice
FRAMEWORK_SEARCH_PATHS/embedding logic is unchanged for real device
builds.

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

* Fix Xcode Cloud ci_post_clone.sh: actually write the secret files

ci_post_clone.sh wrote the literal strings "GOOGLE_SERVICES_PLIST",
"REMOTE_CONFIG_DEFAULTS", and "DEBUG_SERVICE_DATA" into their
respective files -- missing the '$' to dereference the environment
variables entirely, so GoogleService-Info.plist etc. never contained
real content on any Xcode Cloud run. Even with '$' added it would
still be wrong: these vars are base64-encoded in Xcode Cloud's
environment variable settings, same as their proven-working CircleCI
counterparts in .circleci/config.yml's "Setup environment files" step,
and this script never decoded them.

Also: Brainwallet-StoreKit-v1.storekit was never written for Xcode
Cloud at all, unlike CircleCI which creates it from
BRAINWALLET_IOS_STOREKIT_V1_0_FILE alongside the other three files.

Fix: dereference + base64-decode all four env vars into
PreLaunchResources, matching CircleCI's approach, and fail fast with
a clear message if GoogleService-Info.plist doesn't decode to a valid
plist (via PlistBuddy) instead of silently shipping garbage.

Verified via a dry run with real base64-encoded fake values: decoded
content matches exactly, and the PlistBuddy validation passes.

Not yet verified against a live Xcode Cloud run -- no CI report was
provided for this one, this was found via code review (comparing
against CircleCI's already-proven equivalent step). If Xcode Cloud
also runs an Archive/TestFlight workflow (as opposed to only the
brainwalletUITests scheme this script pre-resolves packages for), it
will still need Private/bw-gdlib's BWIOSGdx.xcframework built via the
existing "Build RoboVM" Xcode build phase (runOnlyForDeploymentPostprocessing,
so archive-only) -- worth checking submodule access is granted to
bw-gdlib in Xcode Cloud's source control settings if that workflow
exists and fails.

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

* Fail fast on bad CI secrets instead of a cryptic xcodebuild plist error

Xcode Cloud build 431 (brainwalletUnitTests build-for-testing) failed
90+ seconds into the actual xcodebuild step with:

  error: unable to read input file as a property list: The operation
  couldn't be completed. (SWBUtil.PropertyListConversionError error 2.)
  (in target 'brainwallet' from project 'brainwallet')

during the CopyPlistFile resource-processing step for service-data.plist.
Traced it to the root cause: DEBUG_SERVICE_DATA's value in Xcode Cloud's
environment variable settings (App Store Connect) doesn't base64-decode
to a valid plist. (ci_post_clone.sh's base64 decoding itself is working
correctly -- confirmed from this same build's ci_post_clone.log:
GoogleService-Info.plist and remote-config-defaults.plist, decoded by
the identical code path, copied without error.) That's a secret
misconfigured on Apple's side, outside this repo -- flagging for
whoever manages Xcode Cloud's App Store Connect settings to fix.

What this commit does fix: neither CI script actually verified the
other three generated files were valid before handing them to
xcodebuild, so a bad secret's failure mode was a confusing build-system
error far removed from its actual cause. Extended validation (already
added for GoogleService-Info.plist after the ci_post_clone.sh env-var
bug) to remote-config-defaults.plist and service-data.plist (PlistBuddy)
and Brainwallet-StoreKit-v1.storekit (python3 -m json.tool -- it's JSON,
not a plist, so plutil -lint rejects it outright regardless of validity).
Applied to both ci_scripts/ci_post_clone.sh and .circleci/config.yml's
"Setup environment files" step, since both decode the same four env
vars and either could hit the same class of bug.

Verified via dry runs with real base64-encoded fixtures: all 4 valid ->
clean pass; DEBUG_SERVICE_DATA broken (matching build 431's actual
failure) -> fails immediately with the exact var/file named, instead of
xcodebuild's opaque error later.

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

* Fix locale-dependent test failure in testISO8601DateFormatterFormat

Surfaced on Xcode Cloud build #436, the first build to actually get
brainwalletUnitTests compiling and running there:

  [TEST_FAILURE] NewReceiveTests.swift:192: XCTAssertTrue failed

The test asserted the formatted date string contains the English
month abbreviation "jun", but configured the formatter with
Locale.current -- Xcode Cloud's build agent's default locale doesn't
format months the same way as a developer's en_US Mac, so the
assertion failed for a reason that has nothing to do with the date
formatting logic under test. Fixed by using a deterministic
en_US_POSIX locale, standard practice for locale-independent
date-format tests.

Verified locally: testISO8601DateFormatterFormat passed (0.413s) via
xcodebuild test-without-building -only-testing.

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

* Pin timeZone too -- the locale fix alone wasn't enough

Xcode Cloud build #438 (on af4a2f4, the locale fix) still failed the
exact same assertion:

  [TEST_FAILURE] NewReceiveTests.swift:197: XCTAssertTrue failed
  XCTAssertTrue(formattedString.contains("jun"))

en_US_POSIX fixed the locale half of the problem but not the other
half: formatter.timeZone was never pinned, so it inherited the
executing machine's system time zone. The test date (1717200000) is
June 1 00:00:00 UTC, which is still May 31 in any zone west of UTC:

  TZ=America/Los_Angeles date -r 1717200000  ->  Fri May 31 17:00:00 PDT 2024
  TZ=America/New_York date -r 1717200000     ->  Fri May 31 20:00:00 EDT 2024

My Mac happens to be on BST (UTC+1), where that instant is still
"01 Jun" -- which is exactly why this passed locally both times but
kept failing on Xcode Cloud's build agent, whose system time zone is
apparently west of UTC. Pinned formatter.timeZone to UTC, matching
what the test's own comment already claims the date represents.

Verified locally: testISO8601DateFormatterFormat passed (0.083s).

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

* Stop printing decoded GoogleService-Info.plist content to CI logs

Flagged directly: does anything in this branch leak secrets, given the
repo is public? ci_post_clone.sh itself is clean -- confirmed it only
ever references env var *names*, never literal values, the four
generated files are all gitignored, and none are currently tracked.

But .circleci/config.yml had a pre-existing (not introduced by this
branch) `head -10 GoogleService-Info.plist` right after decoding it,
intended as a "safe" debug dump ("avoid showing sensitive data" per
its own comment) but not actually safe: this repo is public, and
CircleCI serves build logs for public GitHub repos without requiring
a CircleCI login by default, so the first 10 lines of a small XML
plist (API_KEY, GOOGLE_APP_ID, etc. included) were one real CircleCI
run away from being publicly readable.

Replaced with a comment pointing at check_plist (added earlier in
this branch), which already validates the decoded content is a real
plist via PlistBuddy without ever printing it -- the correct way to
verify this.

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

* Fix the same GoogleService-Info.plist log-leak in the other 2 CircleCI configs

0a383bf fixed `head -10 GoogleService-Info.plist` in .circleci/config.yml
(printing decoded secret content into build logs of a public repo, whose
CircleCI logs are viewable without a login by default). Asked to check
the rest of the repo for similar leaks: the exact same block was
copy-pasted into build-uitest-config.yml and daily-develop-config.yml
too, unfixed.

Applied the identical fix to both: content-blind plist validation via
PlistBuddy (check_plist) instead of printing decoded lines. Also added
the StoreKit JSON validation to daily-develop-config.yml, which writes
that file but never validated it (build-uitest-config.yml doesn't
decode a StoreKit file at all, so it doesn't need that check).

Everything else checked clean: no .p8/.storekit/provisioning
profiles/real GoogleService-Info.plist ever committed anywhere in this
repo's current tree; GitHub Actions workflows use native secrets.X
(auto-redacted by GitHub); Fastfile doesn't touch any of these files.

Separately (not fixed here, flagged to the user directly): a real
service-data.plist with actual wallet-ops/AppsFlyer data was committed
in this repo's early history (033e2b4, 6575eb1) and is still reachable
from origin/develop and ~100 branches -- a git-history exposure, not a
CI-log one, and a materially different fix (rotation + possible history
rewrite) that needs the user's explicit direction before anyone touches it.

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>
)

Pulls in core's fix for Firebase Crashlytics issue
9f86d0dd9962b0efc34252c4c41b2659 (gruntsoftware/core#19): EXC_BAD_ACCESS
at address 0x0 in _peerThreadRoutine. pthread_cleanup_pop(1) invoked
ctx->threadCleanup unconditionally, unlike every other optional
callback in the same function, which are null-checked first -- if
threadCleanup was ever unexpectedly NULL, that's a call through a null
function pointer, exactly matching this crash's signature. Now guarded
the same way, falling back to the existing no-op default.

Matches the ef1bda5 precedent: root-cause and fix land in
gruntsoftware/core via its own fix/crashlytics-<hash> branch and PR
(gruntsoftware/core#19, merged, verified there with `make test` --
zero new failures vs. unpatched main), this commit just bumps the
pinned submodule commit to pull it in.

Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
… 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>
@kcw-grunt
kcw-grunt merged commit 9675fdf 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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants