Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a421ac2
chore: add XcodeBuildMCP agent instructions
jvsena42 Aug 27, 2026
5feb27f
test: add journeys harness and Android-parity test identifiers
jvsena42 Aug 27, 2026
f886a25
test: port amount-limits journeys from Android
jvsena42 Aug 27, 2026
23750ac
test: port widgets journeys from Android
jvsena42 Aug 27, 2026
c013835
test: port notification-permission journeys from Android
jvsena42 Aug 27, 2026
8368ea7
test: port cjit-notification journeys from Android
jvsena42 Aug 27, 2026
0b21f6d
test: port hardware-wallet journeys from Android
jvsena42 Aug 27, 2026
8dc098b
docs: note PR #686 follow-up for the amount-limits journeys
jvsena42 Aug 27, 2026
86f9b52
docs: add cross-platform journey guidance
jvsena42 Aug 27, 2026
649d92e
fix: address review on disabled widget tiles and cjit runner docs
jvsena42 Aug 27, 2026
fda9eb9
docs: correct android CLI commands in the cross-platform section
jvsena42 Aug 27, 2026
fe0a4f0
fix: make Language and Transaction Speed settings rows addressable
jvsena42 Aug 27, 2026
3aa006e
docs: record Android input findings from a cross-platform payment
jvsena42 Aug 27, 2026
97cef74
fix: point the hardware activity tag journey at the right controls
jvsena42 Aug 27, 2026
77ba525
docs: reframe journeys as developer assistance and fix harness errors
jvsena42 Aug 28, 2026
84a5c03
fix: correct journey steps found by re-running the corpus
jvsena42 Aug 28, 2026
a28510a
docs: borrow the Android lsp helper via the sibling checkout
jvsena42 Aug 28, 2026
4995e33
docs: note that lsp mine is silent on success
jvsena42 Aug 28, 2026
0d09f38
Merge branch 'master' into chore/xcodebuildmcp-setup
jvsena42 Aug 28, 2026
ad32815
docs: add XcodeBuildMCP setup to the README
jvsena42 Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ buildServer.json
.codex/
.claude/*.local*
.claude/scheduled_tasks.lock

# XcodeBuildMCP
.xcodebuildmcp/config.yaml
126 changes: 124 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,52 @@ This app integrates with:

## Build & Development Commands

### Agent CLI (XcodeBuildMCP)

Agents should prefer the `xcodebuildmcp` CLI over raw `xcodebuild`, `xcrun`, and `simctl`. It wraps the
same toolchain, parses build output, and adds simulator UI automation (AXe is bundled — no separate install).

```bash
# Install
brew tap getsentry/xcodebuildmcp && brew install xcodebuildmcp

# Discover commands and arguments — do not memorize tool lists
xcodebuildmcp --help
xcodebuildmcp <workflow> --help
xcodebuildmcp <workflow> <tool> --help
```

Project defaults live in `.xcodebuildmcp/config.yaml`. It is gitignored because the CLI materializes a
machine-local simulator UDID into it. Generate it with `xcodebuildmcp setup` (interactive), or write it by hand:

```yaml
schemaVersion: 1
sessionDefaults:
projectPath: Bitkit.xcodeproj
scheme: Bitkit
configuration: Debug
simulatorName: iPhone 17
setupPreferences:
platforms: [iOS]
```

With defaults set, most commands need no flags:

```bash
xcodebuildmcp simulator build-and-run # build, install, launch, capture logs (preferred for run intent)
xcodebuildmcp simulator test
xcodebuildmcp simulator snapshot-ui # semantic UI snapshot with elementRefs for tap/type-text
xcodebuildmcp purge --report # scratch storage lives in ~/Library/Developer/XcodeBuildMCP
```

**Notes:**
- There is no standalone `.xcworkspace` here. Use `--project-path Bitkit.xcodeproj`, not `--workspace-path`.
The `-workspace Bitkit.xcodeproj/project.xcworkspace` form in the sections below applies to raw `xcodebuild` only.
- Pass build settings and compilation conditions through `--extra-args`, e.g. for an E2E build:
`--extra-args "SWIFT_ACTIVE_COMPILATION_CONDITIONS=\$(inherited) E2E_BUILD"`.
- `ARCHS` is already pinned to `arm64` in the project, so the arm64-only Rust xcframeworks build fine as long
as a concrete simulator is targeted (`--simulator-name` / `--simulator-id`), never a generic destination.

### Building
```bash
# Standard build - Open Bitkit.xcodeproj in Xcode and build
Expand Down Expand Up @@ -100,11 +146,20 @@ node scripts/validate-translations.js
**Note:** Localization files are synced from Transifex using [bitkit-transifex-sync](https://github.com/synonymdev/bitkit-transifex-sync).

### Testing

```bash
# Run tests via Xcode Test Navigator or:
# Cmd+U in Xcode
# Unit and UI tests — Xcode Test Navigator, Cmd+U, or:
xcodebuildmcp simulator test

# AI device tests (Trezor emulator, developer-triggered) — see Docs/AI_DEVICE_TESTS.md
```

Separately from the test suites, `journeys/` holds XML walkthroughs of app behaviour that an agent
evaluates by driving a running simulator — number pad caps, notification permission, widget flows,
hardware wallet pairing and transfers. They are developer assistance rather than a test layer:
nothing runs them in CI and they gate nothing. Read `journeys/README.md` before running or writing
one, and see the Journeys section under Code Style & Conventions.

## Architecture

### SwiftUI Patterns (CRITICAL)
Expand Down Expand Up @@ -206,6 +261,7 @@ While the project is transitioning away from traditional ViewModels, these still
- `Extensions/`: Swift extensions for utilities and mock data
- `Utilities/`: Helper utilities (Logger, Keychain, Crypto, Haptics, StateLocker)
- `Models/`: Data models (Toast, ElectrumServer, NodeLifecycleState, etc.)
- `journeys/`: XML behaviour specs evaluated by an agent on a simulator (see `journeys/README.md`)
- `Styles/`: Fonts and sheet styles

### Service Queue Pattern
Expand Down Expand Up @@ -290,6 +346,72 @@ Ensure accessibility modifiers and labels are added to custom components.
- Follow Apple's SwiftUI best practices
- AVOID code comments on private functions, types, etc — PREFER self-documenting names that make intent obvious without explanation; only add a comment when the rationale is genuinely non-obvious (e.g. a workaround, an edge case, or a "why" the code itself can't convey)

### Journeys

`journeys/` holds XML behaviour specs evaluated by an agent driving a simulator — the iOS port of
[`bitkit-android/journeys`](https://github.com/synonymdev/bitkit-android/tree/main/journeys). Read
`journeys/README.md` before running or writing one.

- **PORT the journeys whenever you port an Android feature.** If the Android change ships or touches
a journey under `bitkit-android/journeys/`, the iOS PR carries the matching journey. A ported
feature without its journey is an incomplete port.
- KEEP the file name, `<journey name>` and `<action>` prose identical to the Android original so the
two platforms stay diffable. Change only what the platform forces: `adb` becomes `xcodebuildmcp`,
and Android `testTag`s become iOS `accessibilityIdentifier`s.
- MATCH the Android identifier string when adding an `accessibilityIdentifier` for a journey — the
vocabulary is deliberately shared (`N9`, `NRemove`, `SpendingAmountContinue`, `HardwareTransferSign`).
Record any name that cannot match in the table in `journeys/README.md`.
- PAIR a container identifier with `.accessibilityElement(children: .contain)` so it is queryable.
- ADAPT rather than transcribe when iOS genuinely behaves differently, and say so in the journey's
`<description>` and the suite README — never assert Android behaviour iOS does not have.
- SKIP a journey only when the iOS feature does not exist, and record it under "Not ported" in
`journeys/README.md` with what is missing.
- Journeys are developer-assistance specs, not a QA gate. Nothing runs them in CI and no runner is
wired up for them; `ai-device-tests.yml` runs `TrezorBridgeDashboardUITests` and never reads
`journeys/`. An agent runs one on request.
- A journey that disagrees with the app is most likely stale rather than evidence of a bug. Say what
you found and update the journey; escalate only once you have separately confirmed the app is wrong.

#### Running a journey on both platforms

A journey is a shared spec, so when a behaviour is meant to match Android, run the same file on both
sides rather than reasoning about the difference. iOS uses the XcodeBuildMCP CLI above; Android uses
the `android` CLI against a `bitkit-android` checkout, which the `android-cli` agent skill drives:

```bash
android emulator list # AVD names; `start` requires one, it has no default
android emulator start Pixel_9 # or `android run` against a connected device
android layout --pretty # flat JSON of on-screen elements — the snapshot-ui equivalent
android layout --diff # only what changed, to keep context small
android screen capture -o shot.png # secondary; use when layout hits a WebView or animation
```

`android layout` reports each element's `resource-id`, `text`, `content-desc` and `interactions`
(note the hyphens — the JSON keys are not camelCase), so a journey's testTag assertions map onto it
the way the iOS ones map onto `snapshot-ui` identifiers.

The vocabulary really is shared. Settings on both platforms, captured from a live emulator and
simulator, agrees on `Tab-general`, `Tab-security`, `Tab-advanced`, `NavigationBack`, `HeaderMenu`,
`CurrenciesSettings`, `UnitSettings`, `WidgetsSettings` and `QuickpaySettings` — so a comparison run
is mostly signal, and the rows that disagree stand out.

Driving Android input: taps are `adb shell input tap <x> <y>` using an element's `center`. Do not
type long strings — `adb shell input text` silently drops characters (it lost 54 of a 397-character
invoice in testing), and `adb shell cmd clipboard` does not exist on the emulator image. For an
address or invoice, hand it to the app as a URI instead, which also skips the recipient screen:

```bash
adb shell am start -a android.intent.action.VIEW -d "lightning:<invoice>" to.bitkit.dev
```

A cross-platform payment is the sharpest check the two builds agree: take an invoice from one side
(`xcrun simctl pbpaste <udid>` after tapping Copy on iOS) and pay it from the other.

When the two platforms disagree on a journey, write down which it looks like — an intentional
platform difference, or something worth a closer look — in the journey's `<description>` and the
suite README, on both sides, so the next reader does not rediscover it. A disagreement is a prompt to
investigate, not a bug report on its own.

### Changelog

- NEVER edit `CHANGELOG.md` in normal feature/fix PRs; release automation collects changelog fragments into it
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Components/TabBar/TabBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ struct TabBar: View {
TabBarButton(title: t("wallet__send"), icon: "arrow-up", variant: .left) {
onSendPress()
}
.accessibilityIdentifier("Send")

TabBarButton(title: t("wallet__receive"), icon: "arrow-down", variant: .right) {
onReceivePress()
}
.accessibilityIdentifier("Receive")
}
.overlay {
ScanButton {
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Views/Settings/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct GeneralSettingsView: View {
rightText: languageManager.currentLanguageDisplayName
)
}
.accessibilityIdentifier("LanguageSettings")

NavigationLink(value: Route.currencySettings) {
SettingsRow(
Expand Down Expand Up @@ -98,7 +99,6 @@ struct GeneralSettingsView: View {
rightText: settings.defaultTransactionSpeed.title
)
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("TransactionSpeedSettings")

if isPaykitUIActive, pubkyProfile.isAuthenticated {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ struct NotificationsSettings: View {
openPhoneSettings()
}
)
.accessibilityIdentifier("NotificationsOpenSystemSettings")
.padding(.top, 16)

Spacer()
Expand Down
17 changes: 12 additions & 5 deletions Bitkit/Views/Transfer/FundManualAmountView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ struct FundManualAmountView: View {

HStack(alignment: .bottom) {
// Excludes Legacy (not usable for channel funding)
AvailableAmount(label: t("wallet__send_available"), amount: wallet.channelFundableBalanceSats)
.onTapGesture {
amountViewModel.updateFromSats(UInt64(wallet.channelFundableBalanceSats), currency: currency)
}
AvailableAmount(
label: t("wallet__send_available"),
amount: wallet.channelFundableBalanceSats,
testIdentifier: "ExternalAmountAvailable"
)
.onTapGesture {
amountViewModel.updateFromSats(UInt64(wallet.channelFundableBalanceSats), currency: currency)
}

Spacer()

Expand All @@ -72,6 +76,8 @@ struct FundManualAmountView: View {
.accessibilityIdentifier("ExternalAmountContinue")
}
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("ExternalAmount")
.navigationBarHidden(true)
.padding(.horizontal, 16)
.bottomSafeAreaPadding()
Expand All @@ -94,7 +100,8 @@ struct FundManualAmountView: View {
"lightning__spending_amount__error_max__description",
variables: ["amount": CurrencyFormatter.formatSats(fundableBalanceSats)]
),
visibilityTime: Toast.visibilityTimeShort
visibilityTime: Toast.visibilityTimeShort,
accessibilityIdentifier: "ExternalAmountExceededToast"
)
}

Expand Down
5 changes: 4 additions & 1 deletion Bitkit/Views/Transfer/SpendingAdvancedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ struct SpendingAdvancedView: View {
.accessibilityIdentifier("SpendingAdvancedContinue")
}
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("SpendingAdvanced")
.navigationBarHidden(true)
.padding(.horizontal, 16)
.bottomSafeAreaPadding()
Expand Down Expand Up @@ -142,7 +144,8 @@ struct SpendingAdvancedView: View {
"lightning__spending_advanced__error_max__description",
variables: ["amount": CurrencyFormatter.formatSats(transfer.transferValues.maxLspBalance)]
),
visibilityTime: Toast.visibilityTimeShort
visibilityTime: Toast.visibilityTimeShort,
accessibilityIdentifier: "SpendingAdvancedExceededToast"
)
}

Expand Down
25 changes: 18 additions & 7 deletions Bitkit/Views/Transfer/SpendingAmount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,25 @@ struct SpendingAmount: View {
DisplayText(t("lightning__spending_amount__title"), accentColor: .purpleAccent)
.fixedSize(horizontal: false, vertical: true)

NumberPadTextField(viewModel: amountViewModel, showConversion: false)
.onTapGesture {
amountViewModel.togglePrimaryDisplay(currency: currency)
}
.padding(.top, 32)
NumberPadTextField(
viewModel: amountViewModel,
showConversion: false,
testIdentifier: "SpendingAmountNumberField"
)
.onTapGesture {
amountViewModel.togglePrimaryDisplay(currency: currency)
}
.padding(.top, 32)

Spacer()

HStack(alignment: .bottom) {
if let available = availableAmount {
AvailableAmount(label: t("wallet__send_available"), amount: Int(available))
AvailableAmount(
label: t("wallet__send_available"),
amount: Int(available),
testIdentifier: "SpendingAmountAvailable"
)
} else {
HStack(spacing: 4) {
CaptionMText(t("wallet__send_available"))
Expand Down Expand Up @@ -95,6 +103,8 @@ struct SpendingAmount: View {
}
.accessibilityIdentifier("SpendingAmountContinue")
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("SpendingAmount")
.navigationBarHidden(true)
.padding(.horizontal, 16)
.bottomSafeAreaPadding()
Expand Down Expand Up @@ -129,7 +139,8 @@ struct SpendingAmount: View {
"lightning__spending_amount__error_max__description",
variables: ["amount": CurrencyFormatter.formatSats(maxTransferAmount ?? 0)]
),
visibilityTime: Toast.visibilityTimeShort
visibilityTime: Toast.visibilityTimeShort,
accessibilityIdentifier: "SpendingAmountExceededToast"
)
}

Expand Down
1 change: 1 addition & 0 deletions Bitkit/Views/Transfer/SpendingConfirm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ struct SpendingConfirm: View {
Toggle("", isOn: $settings.enableNotifications)
.toggleStyle(SwitchToggleStyle(tint: .purpleAccent))
.labelsHidden()
.accessibilityIdentifier("SpendingConfirmNotificationSwitch")
}
.frame(height: 50)

Expand Down
5 changes: 4 additions & 1 deletion Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct ReceiveCjitAmount: View {
SheetHeader(title: t("wallet__receive_bitcoin"), showBackButton: true)

VStack(alignment: .leading, spacing: 0) {
NumberPadTextField(viewModel: amountViewModel)
NumberPadTextField(viewModel: amountViewModel, testIdentifier: "ReceiveCjitAmountNumberField")
.onTapGesture {
amountViewModel.togglePrimaryDisplay(currency: currency)
}
Expand Down Expand Up @@ -69,7 +69,10 @@ struct ReceiveCjitAmount: View {
await onContinue()
}
}
.accessibilityIdentifier("ReceiveCjitAmountContinue")
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("ReceiveCjitAmount")
.navigationBarHidden(true)
.padding(.horizontal, 16)
.sheetBackground()
Expand Down
3 changes: 3 additions & 0 deletions Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ struct ReceiveCjitConfirmation: View {
Toggle("", isOn: $settings.enableNotifications)
.toggleStyle(SwitchToggleStyle(tint: .brandAccent))
.labelsHidden()
.accessibilityIdentifier("ReceiveConfirmNotificationSwitch")
}
.frame(height: 50)
.padding(.bottom, 8)
Expand All @@ -82,6 +83,8 @@ struct ReceiveCjitConfirmation: View {
}
}
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("ReceiveCjitConfirm")
.navigationBarHidden(true)
.padding(.horizontal, 16)
.sheetBackground()
Expand Down
3 changes: 3 additions & 0 deletions Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ struct ReceiveCjitLearnMore: View {
Toggle("", isOn: $settings.enableNotifications)
.toggleStyle(SwitchToggleStyle(tint: .brandAccent))
.labelsHidden()
.accessibilityIdentifier("ReceiveLiquidityNotificationSwitch")
}
.frame(height: 50)
.padding(.bottom, 8)
Expand All @@ -63,6 +64,8 @@ struct ReceiveCjitLearnMore: View {
dismiss()
}
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("ReceiveCjitLiquidity")
.navigationBarHidden(true)
.padding(.horizontal, 16)
.sheetBackground()
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Views/Wallets/Send/SendAmountView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ struct SendAmountView: View {
.accessibilityIdentifier("ContinueAmount")
}
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("SendAmount")
.navigationBarHidden(true)
.padding(.horizontal, 16)
.sheetBackground()
Expand Down
4 changes: 4 additions & 0 deletions Bitkit/Views/Widgets/WidgetsListSheetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ struct WidgetsListSheetView: View {
guard enabled else { return }
navigationPath.append(.preview(type))
}
.accessibilityElement(children: .combine)
// Tapping is gated on `enabled`, so a dimmed tile must not announce itself as
// actionable — VoiceOver would otherwise offer a double-tap that silently does nothing.
.accessibilityAddTraits(enabled ? AccessibilityTraits.isButton : [])
.accessibilityIdentifier("WidgetListItem-\(type.rawValue)")
}

Expand Down
Loading
Loading