From a421ac250cc84aea150db80aec3466122c6c9038 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 08:28:42 -0300 Subject: [PATCH 01/20] chore: add XcodeBuildMCP agent instructions --- .gitignore | 3 +++ AGENTS.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/.gitignore b/.gitignore index 319dd2402..554706730 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ buildServer.json .codex/ .claude/*.local* .claude/scheduled_tasks.lock + +# XcodeBuildMCP +.xcodebuildmcp/config.yaml diff --git a/AGENTS.md b/AGENTS.md index b3c1d5630..f59e599b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 --help +xcodebuildmcp --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 From 5feb27f7b40a24433925f11735f59128874ca805 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:09:32 -0300 Subject: [PATCH 02/20] test: add journeys harness and Android-parity test identifiers Ports the journey concept from bitkit-android: XML behaviour specs evaluated by an agent driving a simulator through the XcodeBuildMCP CLI. Adds the runner guide, the Android/iOS identifier mapping, and the AGENTS.md rule that an Android port carries its journeys with it. Adds the accessibility identifiers the ported journeys assert on, matching the Android testTag vocabulary: - Screen containers: SendAmount, SpendingAmount, SpendingAdvanced, ExternalAmount, ReceiveCjitAmount, ReceiveCjitConfirm, ReceiveCjitLiquidity - Fields: SpendingAmountNumberField, SpendingAmountAvailable, ExternalAmountAvailable, ReceiveCjitAmountNumberField - Controls: Send, Receive, ReceiveCjitAmountContinue, NotificationsOpenSystemSettings - Over-max toasts: SpendingAmountExceededToast, SpendingAdvancedExceededToast, ExternalAmountExceededToast (Send already had one) - The three "Enable background setup" switches, which shared one binding and no ids Widget list tiles are onTapGesture views, so the runtime UI snapshot did not expose them as tappable targets. They now combine their children and report the button trait, making each tile one addressable element; without the combine the identifier fanned out to every child label. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 39 +++++- Bitkit/Components/TabBar/TabBar.swift | 2 + .../Notifications/NotificationsSettings.swift | 1 + .../Views/Transfer/FundManualAmountView.swift | 17 ++- .../Views/Transfer/SpendingAdvancedView.swift | 5 +- Bitkit/Views/Transfer/SpendingAmount.swift | 25 +++- Bitkit/Views/Transfer/SpendingConfirm.swift | 1 + .../Wallets/Receive/ReceiveCjitAmount.swift | 5 +- .../Receive/ReceiveCjitConfirmation.swift | 3 + .../Receive/ReceiveCjitLearnMore.swift | 3 + .../Views/Wallets/Send/SendAmountView.swift | 2 + .../Views/Widgets/WidgetsListSheetView.swift | 2 + journeys/README.md | 121 ++++++++++++++++++ 13 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 journeys/README.md diff --git a/AGENTS.md b/AGENTS.md index f59e599b9..ac907af32 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,11 +146,22 @@ node scripts/validate-translations.js **Note:** Localization files are synced from Transifex using [bitkit-transifex-sync](https://github.com/synonymdev/bitkit-transifex-sync). ### Testing + +Three layers, in increasing order of setup cost: + ```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 ``` +**Journeys** are the third layer: XML behaviour specs in `journeys/`, evaluated by an agent driving a +running simulator rather than by a test runner. They cover flows that are impractical to assert in +XCUITest — number pad caps, notification permission, widget flows, hardware wallet pairing and +transfers. Read `journeys/README.md` before running or writing one, and see the Journeys section +under Code Style & Conventions for the rule on porting them alongside Android changes. + ## Architecture ### SwiftUI Patterns (CRITICAL) @@ -252,6 +263,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 @@ -336,6 +348,29 @@ 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, `` and `` 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 + `` 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 agent-evaluated and non-deterministic: run them from the manual `ai-device-tests` + workflow, never as a blocking CI gate. + ### Changelog - NEVER edit `CHANGELOG.md` in normal feature/fix PRs; release automation collects changelog fragments into it diff --git a/Bitkit/Components/TabBar/TabBar.swift b/Bitkit/Components/TabBar/TabBar.swift index e112ba082..af04a1d4f 100644 --- a/Bitkit/Components/TabBar/TabBar.swift +++ b/Bitkit/Components/TabBar/TabBar.swift @@ -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 { diff --git a/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift b/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift index eeae11de8..c6aec03c0 100644 --- a/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift +++ b/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift @@ -73,6 +73,7 @@ struct NotificationsSettings: View { openPhoneSettings() } ) + .accessibilityIdentifier("NotificationsOpenSystemSettings") .padding(.top, 16) Spacer() diff --git a/Bitkit/Views/Transfer/FundManualAmountView.swift b/Bitkit/Views/Transfer/FundManualAmountView.swift index 58c7bca73..929001e0a 100644 --- a/Bitkit/Views/Transfer/FundManualAmountView.swift +++ b/Bitkit/Views/Transfer/FundManualAmountView.swift @@ -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() @@ -72,6 +76,8 @@ struct FundManualAmountView: View { .accessibilityIdentifier("ExternalAmountContinue") } } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("ExternalAmount") .navigationBarHidden(true) .padding(.horizontal, 16) .bottomSafeAreaPadding() @@ -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" ) } diff --git a/Bitkit/Views/Transfer/SpendingAdvancedView.swift b/Bitkit/Views/Transfer/SpendingAdvancedView.swift index 03bd9eedf..746c69a67 100644 --- a/Bitkit/Views/Transfer/SpendingAdvancedView.swift +++ b/Bitkit/Views/Transfer/SpendingAdvancedView.swift @@ -98,6 +98,8 @@ struct SpendingAdvancedView: View { .accessibilityIdentifier("SpendingAdvancedContinue") } } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("SpendingAdvanced") .navigationBarHidden(true) .padding(.horizontal, 16) .bottomSafeAreaPadding() @@ -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" ) } diff --git a/Bitkit/Views/Transfer/SpendingAmount.swift b/Bitkit/Views/Transfer/SpendingAmount.swift index f25d5c977..262cdb605 100644 --- a/Bitkit/Views/Transfer/SpendingAmount.swift +++ b/Bitkit/Views/Transfer/SpendingAmount.swift @@ -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")) @@ -95,6 +103,8 @@ struct SpendingAmount: View { } .accessibilityIdentifier("SpendingAmountContinue") } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("SpendingAmount") .navigationBarHidden(true) .padding(.horizontal, 16) .bottomSafeAreaPadding() @@ -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" ) } diff --git a/Bitkit/Views/Transfer/SpendingConfirm.swift b/Bitkit/Views/Transfer/SpendingConfirm.swift index c8f305446..331f5797d 100644 --- a/Bitkit/Views/Transfer/SpendingConfirm.swift +++ b/Bitkit/Views/Transfer/SpendingConfirm.swift @@ -91,6 +91,7 @@ struct SpendingConfirm: View { Toggle("", isOn: $settings.enableNotifications) .toggleStyle(SwitchToggleStyle(tint: .purpleAccent)) .labelsHidden() + .accessibilityIdentifier("SpendingConfirmNotificationSwitch") } .frame(height: 50) diff --git a/Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift b/Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift index 56265f522..bfa9ef31d 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift @@ -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) } @@ -69,7 +69,10 @@ struct ReceiveCjitAmount: View { await onContinue() } } + .accessibilityIdentifier("ReceiveCjitAmountContinue") } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("ReceiveCjitAmount") .navigationBarHidden(true) .padding(.horizontal, 16) .sheetBackground() diff --git a/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift b/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift index 7158683a7..d9f4dfac2 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift @@ -65,6 +65,7 @@ struct ReceiveCjitConfirmation: View { Toggle("", isOn: $settings.enableNotifications) .toggleStyle(SwitchToggleStyle(tint: .brandAccent)) .labelsHidden() + .accessibilityIdentifier("ReceiveConfirmNotificationSwitch") } .frame(height: 50) .padding(.bottom, 8) @@ -82,6 +83,8 @@ struct ReceiveCjitConfirmation: View { } } } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("ReceiveCjitConfirm") .navigationBarHidden(true) .padding(.horizontal, 16) .sheetBackground() diff --git a/Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift b/Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift index a43576ef6..1ff956851 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift @@ -52,6 +52,7 @@ struct ReceiveCjitLearnMore: View { Toggle("", isOn: $settings.enableNotifications) .toggleStyle(SwitchToggleStyle(tint: .brandAccent)) .labelsHidden() + .accessibilityIdentifier("ReceiveLiquidityNotificationSwitch") } .frame(height: 50) .padding(.bottom, 8) @@ -63,6 +64,8 @@ struct ReceiveCjitLearnMore: View { dismiss() } } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("ReceiveCjitLiquidity") .navigationBarHidden(true) .padding(.horizontal, 16) .sheetBackground() diff --git a/Bitkit/Views/Wallets/Send/SendAmountView.swift b/Bitkit/Views/Wallets/Send/SendAmountView.swift index 01311dedf..063cd38f4 100644 --- a/Bitkit/Views/Wallets/Send/SendAmountView.swift +++ b/Bitkit/Views/Wallets/Send/SendAmountView.swift @@ -135,6 +135,8 @@ struct SendAmountView: View { .accessibilityIdentifier("ContinueAmount") } } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("SendAmount") .navigationBarHidden(true) .padding(.horizontal, 16) .sheetBackground() diff --git a/Bitkit/Views/Widgets/WidgetsListSheetView.swift b/Bitkit/Views/Widgets/WidgetsListSheetView.swift index 5a9d14262..85813c37c 100644 --- a/Bitkit/Views/Widgets/WidgetsListSheetView.swift +++ b/Bitkit/Views/Widgets/WidgetsListSheetView.swift @@ -109,6 +109,8 @@ struct WidgetsListSheetView: View { guard enabled else { return } navigationPath.append(.preview(type)) } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) .accessibilityIdentifier("WidgetListItem-\(type.rawValue)") } diff --git a/journeys/README.md b/journeys/README.md new file mode 100644 index 000000000..fabd70136 --- /dev/null +++ b/journeys/README.md @@ -0,0 +1,121 @@ +# Journeys + +A journey is an XML-specified test of the app's behaviour, evaluated by an agent driving a running +simulator. It is the source of truth: if the app disagrees with the journey, the app has failed. + +These are ported from [`bitkit-android/journeys`](https://github.com/synonymdev/bitkit-android/tree/main/journeys) +and deliberately keep the same file names, journey names and `` prose so the two platforms +stay diffable. Only the platform mechanics differ — `adb` becomes `xcodebuildmcp`, and Android +`testTag`s become iOS `accessibilityIdentifier`s (the vocabulary is shared; see [Identifiers](#identifiers)). + +Journeys are agent-evaluated, not deterministic. They belong on the manual `ai-device-tests` +workflow next to the XCUITest suites, **not** on a blocking CI gate. + +## Format + +```xml + + What this proves, and the preconditions needed to prove it. + + Tap the Spending balance card on the home screen + Verify the spending amount screen (id "SpendingAmount") is visible + + +``` + +- Evaluate `` elements in order. The journey passes only if every action passes. +- An action beginning with "check" or "verify" is an expectation about the **current** screen — + inspect it, do not scroll or interact to satisfy it. +- An action that specifies several interactions is split into sub-actions and evaluated individually. +- If an interaction cannot be performed as written, the journey fails. Report the failure; do not + debug around it. +- If the app crashes, exits or freezes, evaluation stops and the journey fails. + +## Running a journey + +Drive the simulator with the XcodeBuildMCP CLI (see the Agent CLI section in `AGENTS.md`): + +```bash +xcodebuildmcp simulator build-and-run # build, install, launch, capture logs +xcodebuildmcp simulator snapshot-ui # semantic snapshot with elementRef targets +xcodebuildmcp ui-automation tap --element-ref e12 # tap one ref from the latest snapshot +xcodebuildmcp ui-automation type-text --element-ref e8 --text "..." +xcodebuildmcp ui-automation wait-for-ui --predicate textContains --text "Spending Balance Maximum" +xcodebuildmcp ui-automation wait-for-ui --identifier SpendingAmount --predicate exists +xcodebuildmcp simulator screenshot # only when a snapshot cannot settle a question +``` + +Assert on identifiers rather than copy where one exists — `--identifier X --predicate exists` fails +fast on a typo, while a text predicate silently depends on the current translation. Note the flag +shape: `--predicate textContains --text "..."`, not `--text-contains`. + +Refresh the snapshot after navigation, scrolling, sheet changes, or any obvious layout change — +`elementRef`s from a stale snapshot are not reusable. + +Prefer `snapshot-ui` over screenshots: it names elements by their accessibility identifier, which is +what the journeys assert on, and it avoids the image-size limits the Android runner has to work around. + +## Backend preconditions + +Most journeys need regtest and a reachable LSP. Start the stack from a sibling `bitkit-docker` +checkout on `main` (same stack `Docs/AI_DEVICE_TESTS.md` uses): + +```bash +cd /path/to/bitkit-docker +docker compose up -d +``` + +Build the app with the E2E compilation condition so it targets the local backend: + +```bash +xcodebuildmcp simulator build-and-run \ + --extra-args "SWIFT_ACTIVE_COMPILATION_CONDITIONS=\$(inherited) E2E_BUILD" +``` + +Fund a wallet before any amount journey — with a zero balance the caps fall back to the global +maximum and the journeys pass for the wrong reason: + +```bash +./lsp POST /regtest/chain/deposit '{"address":"","amountSat":100000}' +./lsp POST /regtest/chain/mine '{"count":3}' +``` + +## Identifiers + +iOS uses `accessibilityIdentifier`; Android uses Compose `testTag`. The vocabulary is shared, so a +journey usually names the same string on both platforms. Where a container needs to be queryable, +iOS pairs the identifier with `.accessibilityElement(children: .contain)`. + +Known naming differences: + +| Concept | Android testTag | iOS accessibilityIdentifier | +| --- | --- | --- | +| Send amount screen | `send_amount_screen` | `SendAmount` | +| Send available balance | `available_balance` | `AvailableAmount` | +| Send max | `SendAmountMax` | *(no button — tap `AvailableAmount`)* | +| External amount available | — | `ExternalAmountAvailable` | + +Everything else — `N0`–`N9`, `N000`, `NDecimal`, `NRemove`, `SpendingAmount*`, `SpendingAdvanced*`, +`External*`, `Hardware*`, `Widget*` — matches Android exactly. + +## Suites + +| Suite | Journeys | Notes | +| --- | --- | --- | +| [amount-limits](amount-limits) | 4 | Number pad caps on all four amount screens | +| [widgets](widgets) | 2 | Widgets intro and add-widget flow | +| [notification-permission](notification-permission) | 4 | "Set up in background" toggles | +| [cjit-notifications](cjit-notifications) | 3 | Adapted — iOS notification copy differs from Android | +| [hardware-wallet](hardware-wallet) | 15 | Trezor over Bridge; see `Docs/AI_DEVICE_TESTS.md` | + +## Not ported + +**`deeplinks` (2 journeys).** The Android journeys exercise `bitkit://screen/...` routing with a +dev-mode gate and a cold-start replay. iOS registers the `bitkit` URL scheme (`Bitkit/Info.plist`) +but `onOpenURL` in `Bitkit/MainNavView.swift` only handles web URLs, Pubky auth callbacks and +payment URIs — there is no screen or sheet deeplink router, and no dev-mode gate to test. These +journeys are blocked on the feature existing, not on the harness. + +## Porting from Android + +When you port an Android feature, port its journeys too — see the Journeys section in `AGENTS.md`. From f886a2556932cb718bd3f3220559bc35ccf54869 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:09:32 -0300 Subject: [PATCH 03/20] test: port amount-limits journeys from Android Four journeys covering the number pad cap on every amount-entry screen: Send, Transfer to Spending, Receiving capacity, and External node funding. Walked end to end on an iPhone 17 simulator. The two screens cap differently and the journeys assert "does not exceed the stated maximum" rather than a value: Send keeps the largest all-9s value under the cap (99 999 against 297 393), while Transfer to Spending clamps to the max exactly (296 522). Delete works at the cap on both and Continue stays enabled throughout. Entry is via the Savings card, not the Spending card as on Android: the iOS Spending screen only offers "Transfer From Savings" while the spending balance is zero, and shows "Transfer To Savings" once a balance exists. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/amount-limits/README.md | 71 +++++++++++++++++++ .../external-amount-over-max.xml | 28 ++++++++ .../send-amount-over-balance.xml | 26 +++++++ .../transfer-spending-advanced-over-max.xml | 26 +++++++ .../transfer-spending-over-max.xml | 29 ++++++++ 5 files changed, 180 insertions(+) create mode 100644 journeys/amount-limits/README.md create mode 100644 journeys/amount-limits/external-amount-over-max.xml create mode 100644 journeys/amount-limits/send-amount-over-balance.xml create mode 100644 journeys/amount-limits/transfer-spending-advanced-over-max.xml create mode 100644 journeys/amount-limits/transfer-spending-over-max.xml diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md new file mode 100644 index 000000000..fb4f030c6 --- /dev/null +++ b/journeys/amount-limits/README.md @@ -0,0 +1,71 @@ +# Amount-limit journeys + +These journeys exercise the "block number pad input exceeding the max/available amount" behaviour. +The same `AmountInputViewModel` cap + `maxExceededCount` effect path backs all four amount-entry +screens (Send, Transfer→Spending, Receiving capacity, External node). + +## What the feature does +- Typing a digit that would push the amount **over the cap is rejected** — the display stays at the + largest value still within the cap (e.g. tapping `9` repeatedly stops at `9 999` when the cap is + `98 064`, because `99 999` would exceed it). +- A short **warning toast** is emitted on the first rejected keypress. +- **Delete is always allowed**, even when sitting at the cap. + +## Mandatory setup +1. **Fund a real, positive available balance first.** With `0` available, the cap falls back to the + global maximum, so nothing gets blocked and the journeys silently pass for the wrong reason. + - Get an on-chain (Savings) address from Receive → Show Details. + - Fund and mine via the regtest backend: + `./lsp POST /regtest/chain/deposit '{"address":"","amountSat":100000}'` + then `./lsp POST /regtest/chain/mine '{"count":3}'`, and wait for the balance to sync. +2. **Transfer/Spending and Receiving-capacity flows need the node connected to the LSP** so a real + max can be quoted. On the Spending amount screen the max starts at `0` behind a spinner — wait + for it to populate before typing. +3. **The external-node flow needs a reachable Lightning peer.** The staging LSP node works + (`./lsp GET /info` for its id/host/port). + +## Gotchas +- **The cap can be lower than the visible "Available".** Fee and channel reserves mean e.g. Available + `99 890` but a spending max of `98 064`. Assert "does not exceed the **stated maximum**", not + "Available". +- **Do not assert Continue is disabled when over the max.** The input is *capped* rather than left in + an over-max state, so the capped value is valid and Continue stays **enabled**. +- Toasts are short-lived (`Toast.visibilityTimeShort`) and will have faded before a snapshot + round-trip completes. Assert one with `xcodebuildmcp ui-automation wait-for-ui` on its identifier + (`SendAmountExceededToast`, `SpendingAmountExceededToast`, `SpendingAdvancedExceededToast`, + `ExternalAmountExceededToast`) immediately after the over-max keypress. +- Prefer `snapshot-ui` and tap elements by identifier (`N9`, `NRemove`) over screen coordinates. + +## Verified behaviour + +Both walked on an iPhone 17 simulator. **The two screens cap differently, so assert "does not exceed +the stated maximum" — never a specific value:** + +| Screen | Available | Nine taps on `N9` | After one `NRemove` | +| --- | --- | --- | --- | +| Send | 297 393 | **99 999** — the largest all-9s value still under the cap | 9 999 | +| Transfer → Spending | 296 522 | **296 522** — clamped to the max exactly | 29 652 | + +Continue stays enabled throughout on both: the cap is applied to the input rather than leaving it in +an invalid state. The `SpendingAmountExceededToast` fires on the first rejected keypress and is gone +well before a snapshot round-trip returns — race a `wait-for-ui` on its identifier against the taps. + +The Transfer → Spending max populates from the on-chain balance and cached Blocktank info, so the +screen is reachable and cappable even with the regtest backend down. That is *not* a substitute for +a real LSP quote: without one the numbers are not the ones a real transfer would use. + +## Identifiers used +- Number pad keys: digits `N0`–`N9`, triple-zero `N000`, decimal `NDecimal`, delete `NRemove`. +- Send: screen `SendAmount`, field `SendNumberField`, available `AvailableAmount`, + continue `ContinueAmount`; recipient `RecipientManual` / `RecipientInput` / `AddressContinue`; + home Send button `Send`. +- Transfer→Spending: screen `SpendingAmount`, field `SpendingAmountNumberField`, + available `SpendingAmountAvailable`, 25% `SpendingAmountQuarter`, max `SpendingAmountMax`, + continue `SpendingAmountContinue`. +- Receiving capacity: screen `SpendingAdvanced`, field `SpendingAdvancedNumberField`, + min/default/max `SpendingAdvancedMin`/`SpendingAdvancedDefault`/`SpendingAdvancedMax`, + continue `SpendingAdvancedContinue`. +- External: funding `FundManual`; connection `NodeIdInput`/`HostInput`/`PortInput`/`ExternalContinue`; + amount screen `ExternalAmount`, field `ExternalAmountNumberField`, + available `ExternalAmountAvailable`, 25% `ExternalAmountQuarter`, max `ExternalAmountMax`, + continue `ExternalAmountContinue`. diff --git a/journeys/amount-limits/external-amount-over-max.xml b/journeys/amount-limits/external-amount-over-max.xml new file mode 100644 index 000000000..20e321959 --- /dev/null +++ b/journeys/amount-limits/external-amount-over-max.xml @@ -0,0 +1,28 @@ + + + Verifies the external node funding amount number pad blocks input exceeding the maximum, shows the + "Spending Balance Maximum" warning toast, and still allows deleting digits while at the cap. + + Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a reachable + external Lightning node to peer with — the staging LSP node works (`./lsp GET /info` for its + id, host and port). Start on the wallet home screen. + + Do NOT assert that Continue is disabled at the over-max input: the input is capped (never left in + an over-max state), so the capped value is valid and Continue stays enabled. + + + Open Settings, then the "Advanced" tab, then "Lightning Connections" + Tap "Get Started" to open the funding options screen + Tap "Manual Setup" (id "FundManual") + Type the node id into the node id field (id "NodeIdInput") + Type the host into the host field (id "HostInput") + Type the port into the port field (id "PortInput") + Tap Continue (id "ExternalContinue") and wait for the peer connection to succeed + Verify the external node amount screen (id "ExternalAmount") is visible and an available amount (id "ExternalAmountAvailable") is displayed + Tap the "9" key (id "N9") nine times to enter an amount far larger than the maximum allowed + Verify a "Spending Balance Maximum" warning toast appears (id "ExternalAmountExceededToast") + Verify the amount in the input field (id "ExternalAmountNumberField") does not exceed the displayed maximum + Tap the delete key (id "NRemove") once + Verify the amount in the input field (id "ExternalAmountNumberField") decreased after the delete + + diff --git a/journeys/amount-limits/send-amount-over-balance.xml b/journeys/amount-limits/send-amount-over-balance.xml new file mode 100644 index 000000000..e812afcd3 --- /dev/null +++ b/journeys/amount-limits/send-amount-over-balance.xml @@ -0,0 +1,26 @@ + + + Verifies the Send amount number pad blocks input exceeding the available balance, shows an + "Insufficient balance" warning toast, and still allows deleting digits while at the cap. + + Precondition: onboarded dev wallet with a known POSITIVE on-chain (Savings) balance — without a + positive balance the max falls back to the global cap and nothing is blocked. Fund ~100 000 sats + via the regtest deposit + mine and wait for it to sync (see README.md). Have a valid regtest + bitcoin address ready to type. Start on the wallet home screen. + + iOS has no separate MAX button on this screen: tapping the available balance sets the max. + + + Tap the Send button (id "Send") + If a camera permission dialog appears, dismiss it by choosing "Don't Allow" + Tap "Enter Manually" (id "RecipientManual") + Type a valid regtest bitcoin address into the recipient field (id "RecipientInput") + Tap Continue (id "AddressContinue") + Verify the Send amount screen (id "SendAmount") is visible and the available balance (id "AvailableAmount") is displayed + Tap the "9" key (id "N9") nine times to enter an amount far larger than the available balance + Verify an "Insufficient balance" warning toast appears (id "SendAmountExceededToast") + Verify the amount in the input field (id "SendNumberField") does not exceed the available balance + Tap the delete key (id "NRemove") once + Verify the amount in the input field (id "SendNumberField") decreased after the delete + + diff --git a/journeys/amount-limits/transfer-spending-advanced-over-max.xml b/journeys/amount-limits/transfer-spending-advanced-over-max.xml new file mode 100644 index 000000000..81b6a6e23 --- /dev/null +++ b/journeys/amount-limits/transfer-spending-advanced-over-max.xml @@ -0,0 +1,26 @@ + + + Verifies the receiving capacity (advanced) number pad blocks input exceeding the maximum LSP + balance, shows the "Receiving Capacity Maximum" warning toast, and still allows deleting digits + while at the cap. + + Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a running node + connected to the LSP. Start on the wallet home screen. The advanced screen is reached by first + setting a valid spending amount and continuing to the confirm screen. + + + Tap the Savings balance card on the home screen (id "ActivitySavings") + Tap "Transfer To Spending" (id "TransferToSpending") + If a transfer intro screen appears, tap "Get Started" + Wait until the spending amount screen (id "SpendingAmount") has loaded a positive maximum (id "SpendingAmountAvailable") + Tap "25%" (id "SpendingAmountQuarter") to set a valid amount within range + Tap Continue (id "SpendingAmountContinue") + On the confirm screen, tap "Advanced" (id "SpendingConfirmAdvanced") + Verify the receiving capacity screen (id "SpendingAdvanced") is visible + Tap the "9" key (id "N9") nine times to enter a capacity far larger than the maximum allowed + Verify a "Receiving Capacity Maximum" warning toast appears (id "SpendingAdvancedExceededToast") + Verify the amount in the input field (id "SpendingAdvancedNumberField") does not exceed the maximum receiving capacity + Tap the delete key (id "NRemove") once + Verify the amount in the input field (id "SpendingAdvancedNumberField") decreased after the delete + + diff --git a/journeys/amount-limits/transfer-spending-over-max.xml b/journeys/amount-limits/transfer-spending-over-max.xml new file mode 100644 index 000000000..8f9f00c23 --- /dev/null +++ b/journeys/amount-limits/transfer-spending-over-max.xml @@ -0,0 +1,29 @@ + + + Verifies the "Transfer to Spending" amount number pad blocks input exceeding the maximum allowed + transfer amount, shows the "Spending Balance Maximum" warning toast, and still allows deleting + digits while at the cap. + + Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a running node + connected to the LSP (so a real max can be quoted). The max starts at 0 behind a spinner — wait + for it to populate. Note the cap may be lower than the visible "Available" due to fees. Start on + the wallet home screen. + + Enter via the Savings card, not the Spending card: the Spending screen only offers + "Transfer From Savings" while the spending balance is zero, and shows "Transfer To Savings" + once a balance exists. The Savings card's "Transfer To Spending" reaches the same screen in + both states. + + + Tap the Savings balance card on the home screen (id "ActivitySavings") + Tap "Transfer To Spending" (id "TransferToSpending") + If a transfer intro screen appears, tap "Get Started" + Verify the spending amount screen (id "SpendingAmount") is visible + Wait until the available/maximum amount (id "SpendingAmountAvailable") finishes loading and shows a positive value + Tap the "9" key (id "N9") nine times to enter an amount far larger than the maximum allowed + Verify a "Spending Balance Maximum" warning toast appears (id "SpendingAmountExceededToast") + Verify the amount in the input field (id "SpendingAmountNumberField") does not exceed the stated maximum + Tap the delete key (id "NRemove") once + Verify the amount in the input field (id "SpendingAmountNumberField") decreased after the delete + + From 23750ac972c8c56554ec66a5e844631469177487 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:09:32 -0300 Subject: [PATCH 04/20] test: port widgets journeys from Android Two journeys covering the widgets intro on first run and the add-widget flow. Both walked end to end on an iPhone 17 simulator with no backend running. The home page "Add Widget" button sits below the fold and needs a scroll before it can be tapped, which Android does not; the sheet scroll Android needs is conditional on iOS since all six tiles fit on an iPhone 17. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/widgets/README.md | 42 +++++++++++++++++++++++++++ journeys/widgets/add-widgets-flow.xml | 15 ++++++++++ journeys/widgets/widgets-intro.xml | 16 ++++++++++ 3 files changed, 73 insertions(+) create mode 100644 journeys/widgets/README.md create mode 100644 journeys/widgets/add-widgets-flow.xml create mode 100644 journeys/widgets/widgets-intro.xml diff --git a/journeys/widgets/README.md b/journeys/widgets/README.md new file mode 100644 index 000000000..7aed53088 --- /dev/null +++ b/journeys/widgets/README.md @@ -0,0 +1,42 @@ +# Widget journeys + +Cover the widgets intro (first run) and the add-widget flow reached from the wallet home screen. + +## Preconditions +- Onboarded dev wallet. No backend or funding is required — widget feeds are read-only. +- **Widgets must be enabled** in Settings → Widgets ("Show Widgets"). When it is off the widget tiles + in the list sheet render disabled and an "Enable in settings" button (id `WidgetEnableInSettings`) + is shown instead, and both journeys fail on the first tap. +- `widgets-intro.xml` needs the intro **unseen** and `add-widgets-flow.xml` needs it **seen**. The + flag is `hasSeenWidgetsIntro` in `UserDefaults`; reset it by reinstalling the app + (`xcodebuildmcp simulator install` over a fresh wallet) or by wiping the simulator's app data. + +## iOS notes +- The drawer menu row is `DrawerWidgets`. With the intro unseen it pushes the intro screen + (id `WidgetsOnboarding`); with the intro seen it opens the home widgets page or the widgets list + sheet, depending on the "Show Widgets" setting. +- Every widget tile routes to the **preview** screen, whether or not the widget has editable options, + so "Save Widget" (id `WidgetSave`) is reachable in one tap from the list for all types. + +## Verified on simulator + +Both journeys were walked on an iPhone 17 simulator with no backend running: + +- The drawer row, the intro screen and its two buttons, the sheet, all six tiles, the preview screen + and Save Widget all resolve, and Save returns to the widgets page with the new widget present. +- **`WidgetsAdd` sits below the fold** on the home widgets page — the journey has to scroll before it + can be tapped, even though `--identifier WidgetsAdd --predicate exists` passes without scrolling. +- All six tiles fit on an iPhone 17 sheet without scrolling, so that step is conditional. +- Widget tiles are `.onTapGesture` views, not buttons. They now carry + `.accessibilityElement(children: .combine)` + `.accessibilityAddTraits(.isButton)` so each is a + single tappable target in the runtime snapshot; without `.combine` the identifier fanned out to + every child label and made identifier-based tapping ambiguous. + +## Identifiers used +- Drawer: `DrawerWidgets`. +- Intro: screen `WidgetsOnboarding`, buttons `WidgetsOnboardingViewOrganize` and + `WidgetsOnboardingAddWidget`. +- Home widgets section: `WidgetsAdd`, edit mode `WidgetsEdit`. +- List sheet: tiles `WidgetListItem-` where type is one of `price`, `news`, `blocks`, `facts`, + `weather`, `calculator`, `suggestions`; disabled-state button `WidgetEnableInSettings`. +- Preview: `WidgetSave`. diff --git a/journeys/widgets/add-widgets-flow.xml b/journeys/widgets/add-widgets-flow.xml new file mode 100644 index 000000000..ecb991adb --- /dev/null +++ b/journeys/widgets/add-widgets-flow.xml @@ -0,0 +1,15 @@ + + Precondition: onboarded dev wallet, widgets enabled, widgets intro already seen. + + Tap the menu icon + Tap "Widgets" (id "DrawerWidgets") + Verify the wallet overview widgets section is visible + Scroll the widgets page down until "Add Widget" (id "WidgetsAdd") is visible — it sits below the saved widgets and the suggestion cards + Verify "Hello, Widgets" is not visible + Tap "Add Widget" (id "WidgetsAdd") + Verify the "Add Widget" sheet is visible + If "Bitcoin Calculator" is not visible, scroll the sheet down until it is + Tap the "Bitcoin Calculator" widget card (id "WidgetListItem-calculator") + Verify the sheet navigates to "Bitcoin Calculator" and "Save Widget" (id "WidgetSave") is visible + + diff --git a/journeys/widgets/widgets-intro.xml b/journeys/widgets/widgets-intro.xml new file mode 100644 index 000000000..a84a64cbe --- /dev/null +++ b/journeys/widgets/widgets-intro.xml @@ -0,0 +1,16 @@ + + Precondition: onboarded dev wallet, widgets enabled, widgets intro unseen. + + Tap the menu icon + Tap "Widgets" (id "DrawerWidgets") + Verify the widgets intro screen (id "WidgetsOnboarding") is visible showing "Hello, Widgets", "View & Organize", and "Add Widget" + Tap "Add Widget" (id "WidgetsOnboardingAddWidget") — this also marks the intro as seen, so the journey cannot be repeated without resetting hasSeenWidgetsIntro + Verify the "Add Widget" sheet is visible with "Bitcoin Price" and "Bitcoin Weather" + Verify the widgets intro remains visible behind the sheet backdrop at the top + Tap the "Bitcoin Weather" widget card (id "WidgetListItem-weather") + Verify the sheet navigates to "Bitcoin Weather" and "Save Widget" (id "WidgetSave") is visible + Tap "Save Widget" (id "WidgetSave") + Verify the wallet overview widgets section is visible + Verify "Bitcoin Weather" appears as the last widget in the current widget set + + From c01383591f15e538dffd1305db09392a2fd8cae0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:09:32 -0300 Subject: [PATCH 05/20] test: port notification-permission journeys from Android Three journeys covering the "Enable background setup" toggle on the Receive CJIT confirm, Receive CJIT liquidity and Transfer Spending confirm screens. Android's fourth journey does not apply: toggling off on iOS only unregisters push and stays in place, it does not deep link into system settings. It is replaced by toggle-off-and-system-settings-route.xml, which asserts that behaviour and then covers the real iOS route into Settings, the button on Settings > Notifications. That screen's identifiers were verified on simulator. All three toggles bind to one setting, so they must be run one per app state. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/notification-permission/README.md | 44 +++++++++++++++++++ ...ceive-cjit-confirm-notification-toggle.xml | 23 ++++++++++ ...ive-cjit-liquidity-notification-toggle.xml | 24 ++++++++++ .../toggle-off-and-system-settings-route.xml | 29 ++++++++++++ ...r-spending-confirm-notification-toggle.xml | 26 +++++++++++ 5 files changed, 146 insertions(+) create mode 100644 journeys/notification-permission/README.md create mode 100644 journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml create mode 100644 journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml create mode 100644 journeys/notification-permission/toggle-off-and-system-settings-route.xml create mode 100644 journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml diff --git a/journeys/notification-permission/README.md b/journeys/notification-permission/README.md new file mode 100644 index 000000000..bdba5cbf3 --- /dev/null +++ b/journeys/notification-permission/README.md @@ -0,0 +1,44 @@ +# Notification-permission journeys + +Verify that the "Enable background setup" toggle — shown on the Receive → CJIT confirm screen, the +Receive → CJIT liquidity screen and the Transfer → Spending confirm screen — drives the iOS +notification authorization prompt, and that the app's own notification settings own the route into +the system Settings app. + +## iOS vs Android + +Android requests `POST_NOTIFICATIONS` at the toggle and, once granted, re-tapping the toggle deep +links into system settings. iOS differs in two ways that these journeys are written around: + +- **The prompt is one-shot and system-owned.** `PushNotificationManager.requestPermission()` calls + `UNUserNotificationCenter.requestAuthorization`, which only shows the alert while the status is + `notDetermined`. Once the user has answered once, toggling on is silent. +- **Toggling off does not open system settings.** `MainNavView`'s `onChange(of:)` simply calls + `notificationManager.unregister()`. The route into the iOS Settings app is a dedicated button on + Settings → Notifications (id `NotificationsOpenSystemSettings`), which is what + `toggle-off-and-system-settings-route.xml` covers instead of Android's + `toggle-off-opens-system-settings.xml`. + +## Preconditions +- Onboarded dev wallet with the node connected to the LSP so a CJIT order can be quoted, and (for + the transfer journey) a positive on-chain Savings balance. +- **Notification authorization must be `notDetermined`** for the three "requests permission" + journeys. The alert is one-shot per install, so reset it by reinstalling the app: + `xcrun simctl uninstall to.bitkit` then `xcodebuildmcp simulator build-and-run`. + Re-onboarding the wallet is part of that reset. +- `settings.enableNotifications` must start **off** so the toggles read unchecked. + +## Gotcha: one setting, three toggles + +All three switches bind to the same `settings.enableNotifications`. Flipping one flips the other two, +so run these journeys one per app state — do not chain them expecting an unchecked toggle on the +second screen. + +## Identifiers used +- Receive CJIT: amount screen `ReceiveCjitAmount`, field `ReceiveCjitAmountNumberField`, + continue `ReceiveCjitAmountContinue`; confirm screen `ReceiveCjitConfirm` with switch + `ReceiveConfirmNotificationSwitch`; liquidity screen `ReceiveCjitLiquidity` with switch + `ReceiveLiquidityNotificationSwitch`. +- Transfer: `SpendingAmount`, `SpendingAmountAvailable`, `SpendingAmountContinue`, and the confirm + screen switch `SpendingConfirmNotificationSwitch`. +- Settings → Notifications: `NotificationsOpenSystemSettings`. diff --git a/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml b/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml new file mode 100644 index 000000000..9c7919a8c --- /dev/null +++ b/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml @@ -0,0 +1,23 @@ + + + Verifies that the "Enable background setup" toggle on the Receive → CJIT confirm screen + (id "ReceiveCjitConfirm") launches the iOS notification authorization alert, and that + allowing it checks the toggle. + + Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be + quoted, notification authorization still notDetermined (reinstall first — the alert is one-shot), + and "Enable background setup" starting off. Start on the wallet home screen. + + + Tap the "Receive" button (id "Receive") on the home screen + Tap the "Spending" tab in the Receive sheet + Tap "Receive Lightning funds" + On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad + Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the CJIT order to be created and the confirm screen (id "ReceiveCjitConfirm") to appear + Verify the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") is visible and off + Tap the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") + Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") + Tap "Allow" + Verify the "Enable background setup" toggle is now on + + diff --git a/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml b/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml new file mode 100644 index 000000000..448813937 --- /dev/null +++ b/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml @@ -0,0 +1,24 @@ + + + Verifies that the "Enable background setup" toggle on the Receive → CJIT liquidity screen + (id "ReceiveCjitLiquidity", reached via "Learn More" from the confirm screen) launches the + iOS notification authorization alert, and that allowing it checks the toggle. + + Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be + quoted, notification authorization still notDetermined (reinstall first — the alert is one-shot), + and "Enable background setup" starting off. Start on the wallet home screen. + + + Tap the "Receive" button (id "Receive") on the home screen + Tap the "Spending" tab in the Receive sheet + Tap "Receive Lightning funds" + On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad + Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the confirm screen to appear + Tap "Learn More" to open the liquidity screen + Verify the liquidity screen (id "ReceiveCjitLiquidity") is visible with the lightning channel illustration and the "Enable background setup" toggle (id "ReceiveLiquidityNotificationSwitch") off + Tap the "Enable background setup" toggle (id "ReceiveLiquidityNotificationSwitch") + Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") + Tap "Allow" + Verify the "Enable background setup" toggle is now on + + diff --git a/journeys/notification-permission/toggle-off-and-system-settings-route.xml b/journeys/notification-permission/toggle-off-and-system-settings-route.xml new file mode 100644 index 000000000..0683e3dfd --- /dev/null +++ b/journeys/notification-permission/toggle-off-and-system-settings-route.xml @@ -0,0 +1,29 @@ + + + iOS counterpart of the Android "toggle off opens system settings" journey. On iOS the + "Enable background setup" toggle does NOT deep link anywhere: switching it off just unregisters + push and leaves the user on the same screen, with no alert (the authorization alert is one-shot + and already answered). The route into the iOS Settings app lives on Settings → Notifications + instead, behind the "Customize in iOS Bitkit Settings" button. + + Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be + quoted, notification authorization ALREADY GRANTED, and "Enable background setup" already on so + the toggle starts checked. Start on the wallet home screen. + + + Tap the "Receive" button (id "Receive") on the home screen + Tap the "Spending" tab in the Receive sheet + Tap "Receive Lightning funds" + On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad + Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the confirm screen (id "ReceiveCjitConfirm") to appear + Verify the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") is visible and on + Tap the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") + Verify the toggle turns off, no permission alert appears, the app does not leave the foreground, and the confirm screen is still shown + Dismiss the Receive sheet and return to the wallet home screen + Open the menu, tap "Settings" (id "DrawerSettings"), then open "Notifications" + Verify the "Get paid when Bitkit is closed" row reflects the toggle being off + Tap the "Customize in iOS Bitkit Settings" button (id "NotificationsOpenSystemSettings") + Verify the iOS Settings app opens on the Bitkit entry, with no in-app navigation and no permission alert + Return to Bitkit and verify the Notifications settings screen is still shown + + diff --git a/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml new file mode 100644 index 000000000..46703a7c9 --- /dev/null +++ b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml @@ -0,0 +1,26 @@ + + + Verifies that the "Enable background setup" toggle on the Transfer → Spending confirm screen + launches the iOS notification authorization alert, and that allowing it checks the toggle. + + Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and the node + connected to the LSP (so a real max can be quoted), notification authorization still + notDetermined (reinstall first — the alert is one-shot), and "Enable background setup" starting + off. The spending max starts at 0 behind a spinner — wait for it to populate. Start on the + wallet home screen. Enter via the Savings card, not the Spending card — the Spending screen + only offers "Transfer From Savings" while the spending balance is zero. + + + Tap the Savings balance card on the home screen (id "ActivitySavings") + Tap "Transfer To Spending" (id "TransferToSpending") + If a transfer intro screen appears, tap "Get Started" + On the spending amount screen (id "SpendingAmount"), wait until the available/maximum amount (id "SpendingAmountAvailable") finishes loading and shows a positive value + Enter an amount within the maximum using the number pad + Tap Continue (id "SpendingAmountContinue") and wait for the spending confirm screen to appear + Verify the "Enable background setup" toggle (id "SpendingConfirmNotificationSwitch") is visible and off + Tap the "Enable background setup" toggle (id "SpendingConfirmNotificationSwitch") + Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") + Tap "Allow" + Verify the "Enable background setup" toggle is now on + + From 8368ea739777b4e727c4f40cec56b092836e807d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:09:32 -0300 Subject: [PATCH 06/20] test: port cjit-notification journeys from Android Three journeys covering single delivery of a CJIT payment notification and the rule that a non-CJIT channel opening is never reported as a received payment. Adapted rather than transcribed. NotificationService maps a Blocktank push type to fixed copy and never formats an amount, so Android's thousands-separator assertions have no iOS counterpart and are dropped. The duplicate-notification and wrong-type assertions port directly against the iOS copy: cjitPaymentArrived renders "Incoming Payment", orderPaymentConfirmed renders "Spending Balance Ready". Android's foreground-service journey becomes an app-backgrounded one. These need a physical device: the extension has to decrypt a real Blocktank payload delivered over APNs. Inspection is via the extension's os_log output, unlike the main app which writes log files into the app group. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/cjit-notifications/README.md | 58 +++++++++++++++++++ .../cjit-background-notification.xml | 26 +++++++++ .../cjit-push-single-notification.xml | 23 ++++++++ ...n-cjit-channel-no-payment-notification.xml | 24 ++++++++ 4 files changed, 131 insertions(+) create mode 100644 journeys/cjit-notifications/README.md create mode 100644 journeys/cjit-notifications/cjit-background-notification.xml create mode 100644 journeys/cjit-notifications/cjit-push-single-notification.xml create mode 100644 journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml diff --git a/journeys/cjit-notifications/README.md b/journeys/cjit-notifications/README.md new file mode 100644 index 000000000..ec422f5e5 --- /dev/null +++ b/journeys/cjit-notifications/README.md @@ -0,0 +1,58 @@ +# CJIT notification journeys + +Verify that a CJIT payment surfaces exactly one notification, and that a regular (non-CJIT) channel +opening is never reported as a received payment. + +## Adapted from Android — read this first + +The Android suite asserts notification **copy** that iOS does not produce. `BitkitNotification/NotificationService.swift` +maps a Blocktank push type to fixed title/body pairs and never formats an amount: + +| Blocktank type | iOS title | iOS body | +| --- | --- | --- | +| `cjitPaymentArrived` | Incoming Payment | Open Bitkit now to receive your payment | +| `incomingHtlc` | Incoming Payment | Open Bitkit now to receive your payment | +| `orderPaymentConfirmed` | Spending Balance Ready | Open Bitkit to start paying anyone, anywhere. | +| `mutualClose` | Spending Balance Expired | Open Bitkit to move funds from spending to savings | +| `wakeToTimeout` | Payment Pending | Open Bitkit to process pending payment | + +Consequences for the port: + +- **Android issue #1 (missing thousands separators) has no iOS counterpart.** iOS never puts an + amount in the notification, so there is no formatting to assert. Those actions are dropped rather + than rewritten into something the app was never meant to do. +- **Android issue #2 (double notification) does port**, as "exactly one notification". iOS has no + foreground service and no `WakeNodeWorker`; the Notification Service Extension is the only poster, + so the duplicate would have to come from a duplicate push. +- **Android issue #3 (channel open reported as a payment) ports directly** and is the strongest of + the three: `orderPaymentConfirmed` must render as "Spending Balance Ready", never "Incoming Payment". +- `cjit-foreground-service-notification.xml` becomes `cjit-background-notification.xml` — iOS has no + foreground service, so the equivalent state is simply "app backgrounded". + +## Preconditions +- **A physical device, not the simulator.** These journeys need a real APNs round trip: the app must + hold a device token Blocktank can push to, and the NSE must decrypt a real Blocktank payload. + Run them on the self-hosted macOS runner used by `.github/workflows/ai-device-tests.yml`. + `test-push-server/` can drive a hand-built push at a known device token when you need to exercise + the NSE without the LSP. +- Onboarded regtest wallet connected to the LSP, with notifications authorized and + "Get paid when Bitkit is closed" enabled in Settings → Notifications. +- A funded CJIT entry ready to pay, and no Lightning channel open yet for the CJIT journeys. + +## Inspecting notifications on iOS + +There is no `dumpsys notification`. Two options, in order of preference: + +1. **Read the extension's own log.** `NotificationService` logs through `os_log`, unlike the main app + (which writes log files into the app group). Stream it while the push lands: + ```bash + xcrun simctl spawn log stream --predicate 'eventMessage CONTAINS "🔔"' # simulator + log stream --predicate 'eventMessage CONTAINS "🔔"' # attached device host + ``` + The `🔔 Configured notification: type=…, title=…` line names the type and title it posted. +2. **Snapshot Notification Center.** Swipe down from the top of the screen and take a + `xcodebuildmcp simulator snapshot-ui`, then count the Bitkit entries and read their text. + +## Identifiers used +- In-app toast: `SpendingBalanceReadyToast`. +- Received payment sheet: `ReceivedTransactionButton`. diff --git a/journeys/cjit-notifications/cjit-background-notification.xml b/journeys/cjit-notifications/cjit-background-notification.xml new file mode 100644 index 000000000..220cbd482 --- /dev/null +++ b/journeys/cjit-notifications/cjit-background-notification.xml @@ -0,0 +1,26 @@ + + + iOS counterpart of the Android foreground-service journey (iOS has no foreground service, so the + equivalent state is the app simply being in the background). + + With the app backgrounded, a CJIT payment that opens a JIT channel must produce EXACTLY ONE + notification, posted by the Notification Service Extension with the cjitPaymentArrived copy: + title "Incoming Payment", body "Open Bitkit now to receive your payment". It must NOT be posted + twice, and must NOT use the orderPaymentConfirmed copy. + + Precondition: physical device with an onboarded regtest wallet connected to the LSP, + notifications authorized, "Get paid when Bitkit is closed" enabled. A funded CJIT entry is ready + to pay (see README.md). No Lightning channel open yet — this is the first, channel-opening payment. + + + Confirm "Get paid when Bitkit is closed" is enabled in Settings → Notifications, then go back to the wallet home screen + Send the app to the background by pressing Home, leaving no Bitkit screen in the foreground + Start streaming the extension log: log stream --predicate 'eventMessage CONTAINS "🔔"' + Pay the CJIT invoice via the regtest backend: ./lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' + Wait up to ~30s for the push to arrive and the notification to be delivered + Verify EXACTLY ONE Bitkit notification is posted for this channel opening, not two + Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" + Verify the streamed log shows a single "Configured notification" line with type=cjitPaymentArrived, and no orderPaymentConfirmed notification alongside it + Tap the notification, bring the app to the foreground, and verify the received-transaction sheet shows the correct amount + + diff --git a/journeys/cjit-notifications/cjit-push-single-notification.xml b/journeys/cjit-notifications/cjit-push-single-notification.xml new file mode 100644 index 000000000..d66747ef5 --- /dev/null +++ b/journeys/cjit-notifications/cjit-push-single-notification.xml @@ -0,0 +1,23 @@ + + + The pure push path. With the app fully killed, a CJIT payment must still surface a single + notification, delivered by the Notification Service Extension — this is the wake-the-node-and- + open-the-channel path, so it must keep working. The notification must use the cjitPaymentArrived + copy: title "Incoming Payment", body "Open Bitkit now to receive your payment". + + Precondition: physical device with an onboarded regtest wallet connected to the LSP, + notifications authorized, "Get paid when Bitkit is closed" enabled. A funded CJIT entry is ready + to pay (see README.md), and the app force-quit from the app switcher so only the push and the + extension run. + + + Force-quit Bitkit from the app switcher so no Bitkit process is running + Start streaming the extension log: log stream --predicate 'eventMessage CONTAINS "🔔"' + Pay the CJIT invoice via the regtest backend: ./lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' + Wait up to ~30s for the push to wake the extension and deliver the notification + Verify EXACTLY ONE Bitkit notification is posted for this CJIT payment + Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" + Verify the streamed log shows exactly one "Configured notification" line, with type=cjitPaymentArrived + Tap the notification to open the app and verify the received amount in the activity list matches the CJIT payment + + diff --git a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml new file mode 100644 index 000000000..4be979221 --- /dev/null +++ b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml @@ -0,0 +1,24 @@ + + + Opening a regular (non-CJIT) Lightning channel must NOT be reported as a received payment: no + "Incoming Payment" notification, and never several duplicates. The correct notification for this + path is the orderPaymentConfirmed copy, "Spending Balance Ready". When the app is in the + foreground the only feedback is the in-app "Spending balance ready" toast. + + Precondition: physical device with an onboarded regtest wallet connected to the LSP and a + positive on-chain balance to fund the transfer. No CJIT entry is involved — this is a manual + Transfer channel opening (orderPaymentConfirmed), not a CJIT receive. + + + Keep the app in the foreground on the wallet home screen + Start streaming the extension log: log stream --predicate 'eventMessage CONTAINS "🔔"' + Open a regular spending channel via Transfer → Spending, funding some sats and confirming + Mine confirmations if required: ./lsp POST /regtest/chain/mine '{"count":3}' + Wait for the ChannelReady event (~5-10s) + Verify an in-app "Spending balance ready" toast appears (id "SpendingBalanceReadyToast") + Open Notification Center and inspect the Bitkit entries + Verify NO "Incoming Payment" notification was posted for this channel opening + Verify any notification posted for it uses the "Spending Balance Ready" copy, and that the streamed log shows type=orderPaymentConfirmed rather than cjitPaymentArrived + Verify there are not multiple duplicate channel-ready notifications stacked in Notification Center + + From 0b21f6d1a8c9170d571c803eb2aa31cee5c70a3b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:09:33 -0300 Subject: [PATCH 07/20] test: port hardware-wallet journeys from Android Fifteen journeys covering pairing, the home tile and detail screen, the Hardware Wallets settings surface, passphrase wallets, wallet-scoped activity, and the watch-only Transfer To Spending flow. Android's usb-reconnect becomes reconnect.xml: iOS cannot do WebUSB and has no USB_DEVICE_ATTACHED intent to inject, so the disconnect and reconnect chain runs through Trezor Bridge and the dev Trezor screen instead. Verified on simulator as far as one allows. The simulator has no Bluetooth LE, so every journey that pairs or reconnects needs the Bridge build or a physical device; the read-only journeys run on a plain simulator build. Two Android assumptions were corrected: iOS settings has no "Payments" section, and the connect intro headline renders uppercase. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/hardware-wallet/README.md | 116 ++++++++++++++++++ .../hardware-wallet/activity-blue-icons.xml | 19 +++ .../activity-detail-hw-tags.xml | 20 +++ journeys/hardware-wallet/connect-flow.xml | 25 ++++ .../hardware-wallet/connect-home-tile.xml | 23 ++++ journeys/hardware-wallet/detail-overview.xml | 21 ++++ .../hardware-wallet/passphrase-duplicate.xml | 24 ++++ .../hardware-wallet/passphrase-pairing.xml | 34 +++++ .../passphrase-settings-remove.xml | 22 ++++ .../passphrase-transfer-to-spending.xml | 36 ++++++ journeys/hardware-wallet/reconnect.xml | 21 ++++ .../settings-hardware-wallets.xml | 32 +++++ .../suggestion-intro-sheet.xml | 26 ++++ .../transfer-to-spending-max-lsp-cap.xml | 22 ++++ .../transfer-to-spending-node-warmup.xml | 19 +++ .../hardware-wallet/transfer-to-spending.xml | 27 ++++ 16 files changed, 487 insertions(+) create mode 100644 journeys/hardware-wallet/README.md create mode 100644 journeys/hardware-wallet/activity-blue-icons.xml create mode 100644 journeys/hardware-wallet/activity-detail-hw-tags.xml create mode 100644 journeys/hardware-wallet/connect-flow.xml create mode 100644 journeys/hardware-wallet/connect-home-tile.xml create mode 100644 journeys/hardware-wallet/detail-overview.xml create mode 100644 journeys/hardware-wallet/passphrase-duplicate.xml create mode 100644 journeys/hardware-wallet/passphrase-pairing.xml create mode 100644 journeys/hardware-wallet/passphrase-settings-remove.xml create mode 100644 journeys/hardware-wallet/passphrase-transfer-to-spending.xml create mode 100644 journeys/hardware-wallet/reconnect.xml create mode 100644 journeys/hardware-wallet/settings-hardware-wallets.xml create mode 100644 journeys/hardware-wallet/suggestion-intro-sheet.xml create mode 100644 journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml create mode 100644 journeys/hardware-wallet/transfer-to-spending-node-warmup.xml create mode 100644 journeys/hardware-wallet/transfer-to-spending.xml diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md new file mode 100644 index 000000000..39a0e8533 --- /dev/null +++ b/journeys/hardware-wallet/README.md @@ -0,0 +1,116 @@ +# Hardware wallet journeys + +Cover pairing a Trezor, the home tile and detail screen, the Hardware Wallets settings surface, +passphrase (hidden) wallets, wallet-scoped activity, and the watch-only Transfer To Spending flow. + +## Transport: Bridge, not USB + +iOS cannot do WebUSB, and `Docs/AI_DEVICE_TESTS.md` explains the consequence: the Trezor User Env +emulator is reached through **Trezor Bridge on the host**, and the simulator talks to that localhost +endpoint. `Bitkit/Services/Trezor/` carries both `TrezorBridgeTransport` and `TrezorBLEManager`; +these journeys drive the Bridge path. + +`usb-reconnect.xml` therefore has no iOS counterpart and is replaced by `reconnect.xml`, which +exercises the same disconnect-indicator and reconnect chain through the dev Trezor screen instead of +an injected `USB_DEVICE_ATTACHED` intent. + +## Setup + +Start the emulator stack from a sibling `bitkit-docker` checkout on `main`: + +```bash +cd /path/to/bitkit-docker +docker compose up -d +./scripts/trezor-emulator start +``` + +For the passphrase journeys, start it with passphrase protection instead: + +```bash +TREZOR_PASSPHRASE_PROTECTION=true ./scripts/trezor-emulator start +``` + +Then build and run with the Bridge environment (mirrors the `xcodebuild` invocation in +`Docs/AI_DEVICE_TESTS.md`): + +```bash +TEST_TREZOR_EMU=1 TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://127.0.0.1:21325 \ +xcodebuildmcp simulator build-and-run \ + --extra-args "SWIFT_ACTIVE_COMPILATION_CONDITIONS=\$(inherited) E2E_BUILD TEST_TREZOR_EMU" +``` + +## Order + +Several journeys mutate pairing state. Run them in this order, or re-pair between runs: + +1. `connect-home-tile.xml` — pairs the emulator; every other journey assumes it ran. +2. `settings-hardware-wallets.xml`, `connect-flow.xml`, `suggestion-intro-sheet.xml` — each forgets + and re-pairs the device, ending paired. +3. `activity-blue-icons.xml`, `activity-detail-hw-tags.xml`, `transfer-to-spending*.xml`, `reconnect.xml`. +4. `passphrase-pairing.xml` → `passphrase-duplicate.xml` → `passphrase-transfer-to-spending.xml` → + `passphrase-settings-remove.xml`. +5. `detail-overview.xml` **last** — its final step forgets the device. + +## Approving prompts on the emulator + +The device blocks until each prompt is acknowledged, once per address type for a passphrase session: + +```bash +../bitkit-docker/scripts/trezor-emulator send-json '{"type":"emulator-press-yes","id":1}' +``` + +## Checking for passphrase leaks + +Unlike the notification extension, the app writes its logs as **files in the app group container**, +not to `os_log`. Resolve the container and grep it: + +```bash +GROUP=$(xcrun simctl get_app_container to.bitkit groups | awk '{print $2}') +grep -rl "bitkit-hidden" "$GROUP" || echo NO_PASSPHRASE_LEAK +``` + +## Verified on simulator + +Walked as far as a simulator allows, against a wallet with two Trezor identities already paired: + +- **The simulator has no Bluetooth LE.** Tapping Continue on the connect intro raises a + "Bluetooth Unsupported — This device does not support Bluetooth Low Energy" dialog and the flow + never reaches the Searching step. Every journey that pairs, re-pairs or reconnects therefore needs + the Bridge build above (`TREZOR_BRIDGE=true`) or a physical device. The read-only journeys — + detail overview, activity, settings listing — run fine on a plain simulator build. +- **iOS settings has no "Payments" section.** The Hardware Wallets row sits in a flat list under the + General tab (id `Tab-general`); the Android journeys' "scroll to Payments" step has been rewritten + to scroll to the row itself. +- **The intro headline renders uppercase** ("ADD YOUR HARDWARE WALLET"), unlike Android's sentence + case, so assert accordingly or match case-insensitively. +- Row identifiers are inconsistent in the app and this is not a typo in the journeys: + `HardwareWalletRowName` has **no** separator, while `HardwareWalletRowDelete_` and + `HardwareWalletRow_` use an underscore. The `` is the full `trezor:` device id. +- Confirmed resolving without the emulator: `HardwareWalletsSettings`, `HardwareWalletsScreen`, + `AddHardwareWallet`, `HardwareWalletIntroScreen`/`Continue`/`Cancel`, `HardwareWalletScreen`, + `HardwareTransferToSpending`, `HardwareTransferAmount` and its Available/25%/MAX/Continue controls, + and the number pad keys. + +## Identifiers used +- Dev Trezor screen: `TrezorScanButton`, `TrezorDevice-bridge`, `TrezorKnownDeviceConnect-bridge`, + `TrezorForgetDevice-bridge`, `TrezorDisconnectButton`, `TrezorSection-DeviceInfo`. +- Connect sheet: `HardwareWalletIntroScreen`/`HardwareWalletIntroContinue`/`HardwareWalletIntroCancel`, + `HardwareWalletSearchingScreen`/`HardwareWalletSearchingCancel`, + `HardwareWalletFoundScreen`/`HardwareWalletFoundConnect`/`HardwareWalletFoundCancel`, + `HardwareWalletPairedScreen`/`HardwareWalletPairedFinish`/`HardwareWalletPairedPassphrase`, + `HardwareWalletLabelInput`, `HardwareWalletPairCodeScreen`. +- Passphrase: `HardwareWalletPassphraseScreen`/`HardwareWalletPassphraseInput`/ + `HardwareWalletPassphraseContinue`/`HardwareWalletPassphraseBack`, + `HardwareWalletPassphrasePairedScreen`, `HwPassphraseError`. +- Settings: `HardwareWalletsSettings`, `HardwareWalletsScreen`, `AddHardwareWallet`, + `HardwareWalletRow_`, `HardwareWalletRowName`, `HardwareWalletRowDelete_`, + `RenameHardwareWalletInput`, `RenameHardwareWalletSave`. +- Home and detail: tile `ActivityHardware`, screen `HardwareWalletScreen`, `RemoveHardwareWallet`, + `RemoveHwWalletDialog`, `HwRemoveKeepBackupToggle`. +- Transfer: `HardwareTransferToSpending`, `HardwareTransferAmount`, + `HardwareTransferAmountAvailable`/`Quarter`/`Max`/`Continue`, `HardwareTransferSign`, + `HardwareTransferSignLearnMore`/`SignAdvanced`/`SignDefault`, + `HardwareTransferOpenTrezorConnect`, `HardwareTransferSigned`, + `HwTransferPassphraseSheet`/`Input`/`Continue`/`Cancel`. +- Activity: `ActivityShowAll`, `ActivityShort-`, tabs `Tab-all`/`Tab-sent`/`Tab-received`/`Tab-other`, + `ActivityTags`, `ActivityTag`, `ActivityTxDetails`. diff --git a/journeys/hardware-wallet/activity-blue-icons.xml b/journeys/hardware-wallet/activity-blue-icons.xml new file mode 100644 index 000000000..554f1dfa2 --- /dev/null +++ b/journeys/hardware-wallet/activity-blue-icons.xml @@ -0,0 +1,19 @@ + + + Verifies hardware wallet on-chain activity in the unified home and All Activity lists, + with wallet-scoped blue icons and tab filters. Requires a paired Bridge emulator with at + least two confirmed receives with distinct amounts and one Transfer To Spending activity. + + + Launch the Bitkit app and go to the wallet home screen + Verify the recent activity list contains at least one blue hardware activity row, then tap "Show All" (id "ActivityShowAll") beneath the activity list + Verify All Activity contains two blue received rows with distinct amounts and one blue Transfer row with subtitle "From Savings" + Verify the blue Transfer row appears exactly once and no default-wallet row repeats its amount or transaction + Tap the blue Transfer row with subtitle "From Savings" + Verify the detail screen shows a blue hardware icon, title "From Savings", and "TO SPENDING", then navigate back + Tap the "Sent" tab (id "Tab-sent") and verify the blue received rows and blue Transfer row are not listed + Tap the "Received" tab (id "Tab-received") and verify both distinct blue received rows are listed while the blue Transfer row is absent + Tap the "Other" tab (id "Tab-other") and verify the blue Transfer row is listed while both blue received rows are absent + Tap the "All" tab (id "Tab-all") and verify both blue received rows and the blue Transfer row are listed again + + diff --git a/journeys/hardware-wallet/activity-detail-hw-tags.xml b/journeys/hardware-wallet/activity-detail-hw-tags.xml new file mode 100644 index 000000000..6cf236be3 --- /dev/null +++ b/journeys/hardware-wallet/activity-detail-hw-tags.xml @@ -0,0 +1,20 @@ + + + Verifies wallet-scoped Core tags and Electrum transaction details for a hardware activity. + Requires a paired Bridge emulator with at least two confirmed hardware receives and the + tag "hwtest" absent from the selected activity before the run. + + + Launch the Bitkit app and go to the wallet home screen + Tap "Show All" (id "ActivityShowAll") beneath the activity list + Tap a blue hardware received row that is not the Transfer row + Verify the activity detail screen shows a blue icon and the selected received amount + Tap "Add Tag", enter "hwtest", and confirm the tag + Verify a tag chip labelled "hwtest" is shown (id "ActivityTag") + Navigate back, reopen the same blue hardware row, and verify the "hwtest" tag is still shown + Tap "Explore" (id "ActivityTxDetails") + Verify the Activity Explorer screen shows an Inputs section and an Outputs section, each with at least one entry + Navigate back to All Activity, open the tag filter (id "ActivityTags"), and select "hwtest" + Verify the tagged blue hardware activity remains listed and the other untagged hardware rows are absent + + diff --git a/journeys/hardware-wallet/connect-flow.xml b/journeys/hardware-wallet/connect-flow.xml new file mode 100644 index 000000000..a96afb330 --- /dev/null +++ b/journeys/hardware-wallet/connect-flow.xml @@ -0,0 +1,25 @@ + + + Verifies the Connect Hardware flow entered from Hardware Wallets settings: forgets the + paired device so it can be rediscovered, opens Settings then Hardware Wallets, taps Add, + and runs Searching, Found and Paired, editing the Label Funds field before finishing. + Confirms the paired device is listed under its custom label, which re-pairs the emulator + so later journeys can run. Requires a paired Bridge emulator (run connect-home-tile.xml + first). + + + Launch the Bitkit app, open the menu, navigate to Settings, then Dev Settings, then tap the "Trezor" row + Tap the forget-device (trash) icon next to the known device (id "TrezorForgetDevice-bridge"), confirming any dialog, so the device becomes discoverable again + Navigate back to Settings, ensure the "General" tab (id "Tab-general") is selected, and tap the "Hardware Wallets" row (id "HardwareWalletsSettings") + Verify the Hardware Wallets screen (id "HardwareWalletsScreen") shows no paired devices + Tap the "Add Hardware Wallet" button (id "AddHardwareWallet") + Verify a sheet opens titled "Hardware Wallet" (id "HardwareWalletIntroScreen"); tap the "Continue" button (id "HardwareWalletIntroContinue") + Verify the sheet advances to the "Connect Device" step (id "HardwareWalletSearchingScreen") headed "Searching for devices" + If iOS shows a Bluetooth or Local Network permission prompt, allow it and continue waiting on the Searching step + Verify the sheet advances to the "Found Device" step (id "HardwareWalletFoundScreen") headed "Found Trezor" within 15 seconds, then tap "Connect" (id "HardwareWalletFoundConnect") + Verify the sheet advances to the "Device Connected" step (id "HardwareWalletPairedScreen") headed "Paired Trezor", showing a balance and an editable "Label Funds" field, with no PIN or pairing prompt + Clear the "Label Funds" field (id "HardwareWalletLabelInput") and type "My Trezor" + Tap the "Finish" button (id "HardwareWalletPairedFinish") and verify the sheet closes back to the Hardware Wallets screen + Verify the Hardware Wallets screen now lists one paired device named "My Trezor" with a green connection indicator + + diff --git a/journeys/hardware-wallet/connect-home-tile.xml b/journeys/hardware-wallet/connect-home-tile.xml new file mode 100644 index 000000000..b2b758e7b --- /dev/null +++ b/journeys/hardware-wallet/connect-home-tile.xml @@ -0,0 +1,23 @@ + + + Pairs the Bridge Trezor emulator through the dev Trezor screen, then verifies the + home-screen hardware wallet tile: name, connection indicator, balance, and the + hardware wallet detail screen. Requires the bitkit-docker Trezor User Env running + and a Bridge-enabled build (see README.md). + + + Launch the Bitkit app and dismiss any onboarding or authentication prompts if a wallet already exists + Open the menu, navigate to Settings (id "DrawerSettings"), then Dev Settings + Tap the "Trezor" row + Tap the "Scan" button (id "TrezorScanButton") + Verify a device named "Bitkit Test Trezor" or "Trezor" (id "TrezorDevice-bridge") appears in the device list within 10 seconds + Tap the listed device to connect + Verify a toast or status shows the device as connected within 15 seconds, with no PIN or pairing prompt + Navigate back to the wallet home screen + Verify a hardware wallet tile (id "ActivityHardware") is shown beneath the SAVINGS and SPENDING tiles, labelled with the device name in caps (e.g. "BITKIT TEST TREZOR") + Verify the hardware tile shows a green connection indicator icon next to its name and a blue bitcoin icon with a sats amount + Tap the hardware wallet tile + Verify the hardware wallet detail screen opens (id "HardwareWalletScreen"), showing the device name with a blue bitcoin icon in the top bar and a balance header + Navigate back to the wallet home screen + + diff --git a/journeys/hardware-wallet/detail-overview.xml b/journeys/hardware-wallet/detail-overview.xml new file mode 100644 index 000000000..a6ce28433 --- /dev/null +++ b/journeys/hardware-wallet/detail-overview.xml @@ -0,0 +1,21 @@ + + + Opens the hardware wallet detail screen from the home tile and verifies its overview: + the top bar, balance header, the Transfer To Spending entry when funds are present, + the grouped activity list, and the Remove device confirm dialog. The final + Remove step forgets the device, so run this last (re-run connect-home-tile.xml to pair + again). Requires a paired Bridge emulator (run connect-home-tile.xml first). + + + Launch the Bitkit app and go to the wallet home screen + Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles + Verify the hardware wallet detail screen opens (id "HardwareWalletScreen"), showing the device name with a blue bitcoin icon in the top bar and a balance header + If a "Transfer To Spending" button is shown (id "HardwareTransferToSpending"), tap it, verify the transfer amount screen opens (id "HardwareTransferAmount") titled "TRANSFER TO SPENDING", then navigate back to the hardware wallet detail screen; otherwise skip this step + If the activity list shows transactions, verify their circular icons are blue, then tap the first one, verify an activity detail screen opens, and navigate back + Tap the "Remove" button labelled with the device name near the bottom of the screen (id "RemoveHardwareWallet") + Verify a confirm dialog appears (id "RemoveHwWalletDialog") explaining that your funds are safe and your coins won't be deleted + Tap "Cancel" and verify the hardware wallet detail screen is still shown + Tap the "Remove" button labelled with the device name again, then tap "Remove" in the confirm dialog + Verify the app returns to the wallet home screen and no hardware wallet tile is shown + + diff --git a/journeys/hardware-wallet/passphrase-duplicate.xml b/journeys/hardware-wallet/passphrase-duplicate.xml new file mode 100644 index 000000000..8efe574ae --- /dev/null +++ b/journeys/hardware-wallet/passphrase-duplicate.xml @@ -0,0 +1,24 @@ + + + Re-entering a passphrase that is already watched must not add a second tile for the same + wallet: Bitkit reports it as already added and the hardware tile count stays unchanged. + Requires the emulator started with passphrase protection enabled and the hidden wallet from + passphrase-pairing.xml already paired. + + + Launch the Bitkit app and go to the wallet home screen + Count the hardware wallet tiles (id "ActivityHardware") shown beneath the SAVINGS and SPENDING tiles and remember the number + Open the menu, navigate to Settings, ensure the "General" tab (id "Tab-general") is selected, and tap the "Hardware Wallets" row (id "HardwareWalletsSettings") + Tap the "Add Hardware Wallet" button (id "AddHardwareWallet"), tap "Continue" (id "HardwareWalletIntroContinue"), wait for the Found Device step and tap "Connect" (id "HardwareWalletFoundConnect") + On the "Device Connected" step tap "Passphrase" (id "HardwareWalletPairedPassphrase") + Type the already paired passphrase "bitkit-hidden" into the passphrase field (id "HardwareWalletPassphraseInput") and tap "Continue" (id "HardwareWalletPassphraseContinue") + + Confirm on the emulated device for every account the passphrase session reads: the device + prompts once per address type and the call blocks until it is acknowledged. Either tap the + emulator UI or run: ../bitkit-docker/scripts/trezor-emulator send-json '{"type":"emulator-press-yes","id":1}' + + Verify an error toast reports the passphrase wallet is already being watched, and that the sheet stays on the Passphrase step (id "HardwareWalletPassphraseScreen") with an empty input + Dismiss the sheet and return to the wallet home screen + Verify the number of hardware wallet tiles is unchanged from the count noted at the start + + diff --git a/journeys/hardware-wallet/passphrase-pairing.xml b/journeys/hardware-wallet/passphrase-pairing.xml new file mode 100644 index 000000000..d27d7d51c --- /dev/null +++ b/journeys/hardware-wallet/passphrase-pairing.xml @@ -0,0 +1,34 @@ + + + Adds a passphrase (hidden) wallet of an already paired Trezor: from the Paired step the + Passphrase button opens Enter Passphrase, and entering one watches the wallet it unlocks as a + separate identity with its own funds label. Verifies the home screen then shows two hardware + tiles and counts both in the headline balance. Requires the emulator started with passphrase + protection enabled (TREZOR_PASSPHRASE_PROTECTION=true ./scripts/trezor-emulator start) and a + paired Bridge emulator (run connect-home-tile.xml first). + + + Launch the Bitkit app, open the menu, navigate to Settings, ensure the "General" tab (id "Tab-general") is selected, and tap the "Hardware Wallets" row (id "HardwareWalletsSettings") + Note the paired-device count shown, then tap the "Add Hardware Wallet" button (id "AddHardwareWallet") + Tap "Continue" (id "HardwareWalletIntroContinue") and wait for the Found Device step (id "HardwareWalletFoundScreen"), then tap "Connect" (id "HardwareWalletFoundConnect") + Verify the sheet reaches the "Device Connected" step (id "HardwareWalletPairedScreen"), showing both a "Passphrase" button (id "HardwareWalletPairedPassphrase") and a "Finish" button (id "HardwareWalletPairedFinish") + Tap "Passphrase" (id "HardwareWalletPairedPassphrase") + Verify the Passphrase step opens (id "HardwareWalletPassphraseScreen") headed "Enter passphrase", showing the shield illustration, and that "Continue" (id "HardwareWalletPassphraseContinue") is disabled while the input is empty + Type "bitkit-hidden" into the passphrase field (id "HardwareWalletPassphraseInput"), then tap "Continue" (id "HardwareWalletPassphraseContinue") + + Confirm on the emulated device for every account the passphrase session reads: the device + prompts once per address type and the call blocks until it is acknowledged. Either tap the + emulator UI or run: ../bitkit-docker/scripts/trezor-emulator send-json '{"type":"emulator-press-yes","id":1}' + + Verify the sheet advances to the passphrase paired step (id "HardwareWalletPassphrasePairedScreen") headed "Passphrase funds found", showing a balance and an editable "Label Funds" field prefilled with the device name + Clear the "Label Funds" field (id "HardwareWalletLabelInput") and type "Hidden Trezor" + Tap "Finish" (id "HardwareWalletPairedFinish") and verify the sheet closes + Navigate to the wallet home screen and verify two hardware wallet tiles are shown beneath the SAVINGS and SPENDING tiles, one of them labelled "Hidden Trezor" + Verify the headline total balance is at least the sum of both hardware tile balances + Tap the "Hidden Trezor" tile and verify its hardware wallet detail screen opens (id "HardwareWalletScreen") titled "Hidden Trezor" + Resolve the app group container and grep it for the passphrase: GROUP=$(xcrun simctl get_app_container booted to.bitkit groups | awk '{print $2}'); grep -rl "bitkit-hidden" "$GROUP" || echo NO_PASSPHRASE_LEAK + Verify the previous command printed NO_PASSPHRASE_LEAK: the passphrase must never reach the app logs or the app group storage, only the device session + Grep the newest app log for errors: grep -iE "error|exception" "$(ls -t "$GROUP"/logs/*.log | head -1)" | tail -20 + Verify the previous command reported no Trezor connect, session or watcher errors while the hidden wallet was paired + + diff --git a/journeys/hardware-wallet/passphrase-settings-remove.xml b/journeys/hardware-wallet/passphrase-settings-remove.xml new file mode 100644 index 000000000..7bf6b0cbb --- /dev/null +++ b/journeys/hardware-wallet/passphrase-settings-remove.xml @@ -0,0 +1,22 @@ + + + Verifies that a passphrase wallet is a first-class identity in settings: the settings count + includes it, the Hardware Wallets screen lists it as its own row with its own rename and + delete, and removing it leaves the standard wallet of the same physical device paired and + watched. Requires the hidden wallet from passphrase-pairing.xml already paired. + + + Launch the Bitkit app, open the menu, and navigate to Settings (id "DrawerSettings") + Ensure the "General" tab (id "Tab-general") is selected, scroll to the "Hardware Wallets" row, and verify it (id "HardwareWalletsSettings") shows a count of at least 2 + Tap the "Hardware Wallets" row and verify the screen opens (id "HardwareWalletsScreen") listing two rows, one of them named "Hidden Trezor" + Verify each row shows its own balance and connection indicator, and that the two balances differ + Tap the "Hidden Trezor" row name to open the rename sheet, clear the input (id "RenameHardwareWalletInput"), type "Hidden Funds", and tap Save (id "RenameHardwareWalletSave") + Verify only the hidden wallet row was renamed to "Hidden Funds" and the standard wallet row kept its own name + Tap the delete (trash) icon on the "Hidden Funds" row, and confirm "Remove" in the dialog (id "RemoveHwWalletDialog") + Verify the Hardware Wallets screen now lists exactly one row, the standard wallet, still showing its balance + Navigate to the wallet home screen and verify a single hardware wallet tile remains with a non-zero balance + Count the stored Trezor credentials: GROUP=$(xcrun simctl get_app_container booted to.bitkit groups | awk '{print $2}'); ls "$GROUP"/trezor-thp-credentials/ 2>/dev/null | wc -l + Verify the previous command reported at least one credential file: removing one identity must not unpair the physical device + Tap the remaining hardware wallet tile and verify its detail screen opens and lists its activity, confirming the device was not re-paired + + diff --git a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml b/journeys/hardware-wallet/passphrase-transfer-to-spending.xml new file mode 100644 index 000000000..4c7e71a02 --- /dev/null +++ b/journeys/hardware-wallet/passphrase-transfer-to-spending.xml @@ -0,0 +1,36 @@ + + + Drives Transfer To Spending from a passphrase (hidden) wallet. While the Trezor session that + holds the passphrase is still open the transfer signs straight away; after the session is + dropped Bitkit asks for the passphrase again, and a wrong one is refused instead of signing + from whichever wallet the device happens to have open. Requires the emulator started with + passphrase protection enabled, the hidden wallet from passphrase-pairing.xml paired, and its + native-segwit account holding spendable regtest funds. + + + Launch the Bitkit app and go to the wallet home screen + Tap the hidden wallet tile ("Hidden Trezor") and verify its detail screen opens (id "HardwareWalletScreen") + Tap "Transfer To Spending" (id "HardwareTransferToSpending"), and if the first-run intro is shown tap "Get Started" + Tap the "25%" quick button (id "HardwareTransferAmountQuarter"), then "Continue" (id "HardwareTransferAmountContinue") and wait for the Blocktank order + Verify the sign screen opens (id "HardwareTransferSign") titled "SIGN WITH YOUR DEVICE" + Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and verify no passphrase sheet appears, because the session opened at pairing still holds the hidden wallet + Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator in order, and verify the transaction signed screen appears (id "HardwareTransferSigned") + Wait for the Processing Payment screen, tap "Continue Using Bitkit", and verify the app returns to the wallet home screen + Force-quit Bitkit: xcodebuildmcp simulator stop + Relaunch it: xcodebuildmcp simulator launch-app + Once the app is back on the home screen, open the hidden wallet tile and start Transfer To Spending again, setting an amount with the "25%" quick button and continuing to the sign screen + Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and verify the passphrase sheet opens (id "HwTransferPassphraseSheet"), because the session that held the passphrase is gone + Type a wrong passphrase "not-the-one" into the input (id "HwTransferPassphraseInput") and tap "Continue" (id "HwTransferPassphraseContinue") + + Confirm on the emulated device for every account the passphrase session reads: the device + prompts once per address type and the call blocks until it is acknowledged. Either tap the + emulator UI or run: ../bitkit-docker/scripts/trezor-emulator send-json '{"type":"emulator-press-yes","id":1}' + + Verify an error toast says the passphrase opens a different wallet, that no signing prompt was shown on the emulator, and that no new transfer appears in the activity list + Navigate to the wallet home screen and verify the hardware tile count is unchanged: the wallet the wrong passphrase opened must not be added + Reopen the sign screen, tap "Open Trezor Connect", type the correct passphrase "bitkit-hidden" and tap "Continue" + Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator, and verify the transaction signed screen appears (id "HardwareTransferSigned") + Grep the app group for both passphrases: GROUP=$(xcrun simctl get_app_container booted to.bitkit groups | awk '{print $2}'); grep -rl -e "bitkit-hidden" -e "not-the-one" "$GROUP" || echo NO_PASSPHRASE_LEAK + Verify the previous command printed NO_PASSPHRASE_LEAK: neither the correct nor the rejected passphrase may be written to the logs or the app group storage + + diff --git a/journeys/hardware-wallet/reconnect.xml b/journeys/hardware-wallet/reconnect.xml new file mode 100644 index 000000000..7624dcb46 --- /dev/null +++ b/journeys/hardware-wallet/reconnect.xml @@ -0,0 +1,21 @@ + + + iOS counterpart of the Android USB reconnect journey. iOS cannot do WebUSB and has no + USB_DEVICE_ATTACHED intent to inject, so the transport here is Trezor Bridge and the + disconnect/reconnect chain is driven from the dev Trezor screen instead. Covers the grey + disconnect indicator on the home tile, the serialized reconnect retry loop, and the + prompt-free reconnect for an already paired wallet. Requires a paired Bridge emulator + (run connect-home-tile.xml first). + + + Launch the Bitkit app, open the menu, navigate to Settings, then Dev Settings, then tap the "Trezor" row + Verify the connected device is shown, then tap the "Disconnect" button (id "TrezorDisconnectButton") + Navigate back to the wallet home screen + Verify the hardware wallet tile (id "ActivityHardware") still shows its name and balance but the connection indicator icon is grey + Open the menu, navigate to Settings, then Dev Settings, then tap the "Trezor" row again + Tap the known-device connect entry (id "TrezorKnownDeviceConnect-bridge") to reconnect + Verify the device reconnects within 15 seconds, with no PIN or pairing prompt shown + Navigate back to the wallet home screen + Verify the hardware tile connection indicator is green again + + diff --git a/journeys/hardware-wallet/settings-hardware-wallets.xml b/journeys/hardware-wallet/settings-hardware-wallets.xml new file mode 100644 index 000000000..d8370b8af --- /dev/null +++ b/journeys/hardware-wallet/settings-hardware-wallets.xml @@ -0,0 +1,32 @@ + + + Verifies the Hardware Wallets settings surface: the Hardware Wallets row with the paired-device + count, the Hardware Wallets screen listing the paired device (name, balance, connection + indicator), the Add Hardware Wallet button opening the connect intro sheet, and the + per-row delete confirm dialog. The final Remove forgets the device, so this re-pairs the + emulator at the end so other journeys can run afterwards. Requires a paired Bridge + emulator (run connect-home-tile.xml first). + + + Launch the Bitkit app, open the menu, and navigate to Settings (id "DrawerSettings") + Ensure the "General" tab (id "Tab-general") is selected and scroll down to the "Hardware Wallets" row + Verify the "Hardware Wallets" row (id "HardwareWalletsSettings") shows a numeric value of at least 1 + Tap the "Hardware Wallets" row + Verify the Hardware Wallets screen opens (id "HardwareWalletsScreen") with the top bar titled "Hardware Wallets" + Verify the paired device is listed with its name, a bitcoin balance prefixed with the ₿ symbol, and a green connection indicator on the left + Tap the paired device name on the Hardware Wallets screen + Verify the "Rename Hardware Wallet" sheet opens (id "RenameHardwareWalletInput") with the current name prefilled + Clear the Name field, type "Renamed Trezor", tap "Save" (id "RenameHardwareWalletSave"), and verify the sheet closes + Verify the Hardware Wallets screen now lists the paired device as "Renamed Trezor" + Tap the "Add Hardware Wallet" button near the bottom (id "AddHardwareWallet") + Verify a sheet opens titled "Hardware Wallet" (id "HardwareWalletIntroScreen") showing hardware device illustrations, then tap Cancel (id "HardwareWalletIntroCancel") and verify the sheet closes back to the Hardware Wallets screen + Tap the trash (delete) icon on the device row + Verify a confirm dialog appears (id "RemoveHwWalletDialog") explaining that your funds are safe and your coins won't be deleted + Tap "Cancel" and verify the device is still listed on the Hardware Wallets screen + Tap the trash (delete) icon again, then tap "Remove" in the confirm dialog + Verify the device is removed: the list no longer shows it and an empty state is displayed + Navigate back to Settings and verify the "Hardware Wallets" row value decreased (or the row shows 0) + Open the menu, navigate to Settings, then Dev Settings, then tap the "Trezor" row, tap "Scan" (id "TrezorScanButton"), and tap the discovered device to re-pair it + Verify the device connects within 15 seconds + + diff --git a/journeys/hardware-wallet/suggestion-intro-sheet.xml b/journeys/hardware-wallet/suggestion-intro-sheet.xml new file mode 100644 index 000000000..0ba46d0b1 --- /dev/null +++ b/journeys/hardware-wallet/suggestion-intro-sheet.xml @@ -0,0 +1,26 @@ + + + Verifies the no-device home state and the full Connect Hardware flow: forgetting the + paired device removes the hardware tile, the Hardware suggestion card appears, and + tapping it opens the connect intro sheet. Continuing runs Searching, Found and Paired, + which re-pairs the emulator at the end so other journeys can run afterwards. Requires a + paired Bridge emulator. + + + Launch the Bitkit app, open the menu, navigate to Settings, then Dev Settings, then tap the "Trezor" row + Tap the forget-device (trash) icon next to the known device (id "TrezorForgetDevice-bridge"), confirming any dialog + Navigate back to the wallet home screen + Verify no hardware wallet tile (id "ActivityHardware") is shown beneath the SAVINGS and SPENDING tiles + Scroll the suggestion cards horizontally until a card titled "Hardware" with the text "Connect device" is visible + Tap the "Hardware" suggestion card + Verify a sheet opens titled "Hardware Wallet" (id "HardwareWalletIntroScreen") showing hardware device illustrations + Verify the headline reads "ADD YOUR HARDWARE WALLET" — iOS renders it uppercase — with the words "HARDWARE WALLET" in blue + Tap the "Continue" button (id "HardwareWalletIntroContinue") and verify the sheet advances to the "Connect Device" step (id "HardwareWalletSearchingScreen") headed "Searching for devices", showing a loading animation and a "Cancel" button (id "HardwareWalletSearchingCancel") + If iOS shows a Bluetooth or Local Network permission prompt, allow it and continue waiting on the Searching step + Verify the sheet advances to the "Found Device" step (id "HardwareWalletFoundScreen") within 15 seconds, headed "Found Trezor" with a "Connect" button + Tap the "Connect" button (id "HardwareWalletFoundConnect") + Verify the sheet advances to the "Device Connected" step (id "HardwareWalletPairedScreen") headed "Paired Trezor" within 15 seconds, showing a balance and an editable "Label Funds" field (id "HardwareWalletLabelInput") defaulting to the device name, with no PIN or pairing prompt + Tap the "Finish" button (id "HardwareWalletPairedFinish") and verify the sheet closes back to the home screen + Verify the hardware wallet tile reappears beneath the SAVINGS and SPENDING tiles with the device name and a green connection indicator + + diff --git a/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml b/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml new file mode 100644 index 000000000..e8f7fdc1f --- /dev/null +++ b/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml @@ -0,0 +1,22 @@ + + + Verifies that a funded hardware wallet whose balance is larger than the current + Blocktank/LSP headroom is capped by the transferable spending limit, not by the + device balance. Requires a paired Bridge emulator or physical Trezor with a hardware + balance larger than the available transfer limit, and at least one existing channel or + pending order consuming most of the regtest LSP cap. + + + Launch the Bitkit app and go to the wallet home screen + Verify the hardware wallet tile shows a balance larger than the AVAILABLE amount expected in the transfer flow + Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles, and verify the hardware wallet detail screen opens (id "HardwareWalletScreen") + Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") + Verify the transfer amount screen opens (id "HardwareTransferAmount"), titled "TRANSFER TO SPENDING", and the AVAILABLE amount (id "HardwareTransferAmountAvailable") is lower than the hardware wallet balance because it is capped by LSP headroom + Tap the "MAX" quick button (id "HardwareTransferAmountMax") + Verify the amount field matches the AVAILABLE amount exactly and does not use the full hardware wallet balance + Tap "Continue" (id "HardwareTransferAmountContinue") and wait for the Blocktank order to be created + Verify the sign screen opens (id "HardwareTransferSign"), showing NETWORK FEES, SERVICE FEES, TO SPENDING and TOTAL, without an insufficient-funds toast + Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and approve every hardware-wallet prompt + Verify the transaction signed screen appears (id "HardwareTransferSigned") and then advances to Processing Payment / setting-up progress + + diff --git a/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml b/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml new file mode 100644 index 000000000..996a681f5 --- /dev/null +++ b/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml @@ -0,0 +1,19 @@ + + + Verifies that starting a hardware-wallet transfer while the Lightning node is still + warming up does not fail or strand the CTA in loading. The flow should show the normal + loading/progress UI, continue once the node reaches the needed state, and land on the + sign screen. Requires a paired and funded hardware wallet. + + + Force-quit Bitkit: xcodebuildmcp simulator stop + Relaunch it: xcodebuildmcp simulator launch-app + As soon as the wallet home screen is visible, tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles + Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") + Verify the transfer amount screen or loading/progress UI appears, and no reconnect, node-not-ready, or generic failure toast is shown while the node warms up + Tap the "25%" quick button (id "HardwareTransferAmountQuarter") if the amount screen is shown + Tap "Continue" (id "HardwareTransferAmountContinue") if the amount screen is shown, then wait for order creation and node warm-up to finish + Verify the sign screen opens (id "HardwareTransferSign"), titled "SIGN WITH YOUR DEVICE", and the Continue/Open Trezor Connect CTA is no longer loading + Navigate back without signing so this journey only covers node warm-up behavior + + diff --git a/journeys/hardware-wallet/transfer-to-spending.xml b/journeys/hardware-wallet/transfer-to-spending.xml new file mode 100644 index 000000000..6710395ec --- /dev/null +++ b/journeys/hardware-wallet/transfer-to-spending.xml @@ -0,0 +1,27 @@ + + + Drives the watch-only Transfer To Spending flow for a paired Trezor: Amount -> Sign With + Your Device -> Transaction Signed -> Processing Payment, then verifies the new transfer is + represented once in the hardware wallet's Core activity scope. Requires a paired Bridge + emulator whose native-segwit account holds spendable regtest funds and at least one older + hardware receive row with a distinct amount. + + + Launch the Bitkit app and go to the wallet home screen + Verify the recent activity list contains an older blue hardware received row with a distinct amount + Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles, and verify the hardware wallet detail screen opens (id "HardwareWalletScreen") + Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") + If the first-run Transfer To Spending intro is shown, tap "Get Started" + Verify the transfer amount screen opens (id "HardwareTransferAmount"), titled "TRANSFER TO SPENDING", showing an AVAILABLE row (id "HardwareTransferAmountAvailable"), the 25% and MAX quick buttons, and a number pad + Tap the "25%" quick button (id "HardwareTransferAmountQuarter") to set a valid amount below the available limit + Tap "Continue" (id "HardwareTransferAmountContinue") and wait for the Blocktank order to be created + Verify the sign screen opens (id "HardwareTransferSign"), titled "SIGN WITH YOUR DEVICE", showing the NETWORK FEES, SERVICE FEES, TO SPENDING and TOTAL cells, the Learn More and Advanced buttons, and the Trezor illustration + Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") + Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator in order + Verify the transaction signed screen appears (id "HardwareTransferSigned"), titled "TRANSACTION SIGNED", showing the same fee cells and the checkmark illustration + Wait for the screen to auto-forward and verify the Processing Payment / setting-up progress screen appears + Tap "Continue Using Bitkit" and verify the app returns to the wallet home screen + Verify the recent activity list contains exactly one new blue hardware Transfer row with subtitle "From Savings", above the older blue received row + Tap the new blue Transfer row and verify its detail screen shows "TO SPENDING" + + From 8dc098b6640089121a542fc4a1c2412fd4ba7f95 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:15:34 -0300 Subject: [PATCH 08/20] docs: note PR #686 follow-up for the amount-limits journeys #686 settles both transfer maximums and adds to SpendingAdvancedView the snap-to-max that SpendingAmount.onMaxExceeded() already performs on master. The existing assertion holds either way, but the disabled pad while settling, the snap down to the settled value, and the clamp when Max is tapped early are not covered yet. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/amount-limits/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index fb4f030c6..269afb716 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -54,6 +54,19 @@ The Transfer → Spending max populates from the on-chain balance and cached Blo screen is reachable and cappable even with the regtest backend down. That is *not* a substitute for a real LSP quote: without one the numbers are not the ones a real transfer would use. +## Pending: PR #686 + +[#686](https://github.com/synonymdev/bitkit-ios/pull/686) settles both transfer maximums and adds to +`SpendingAdvancedView` the snap that `SpendingAmount.onMaxExceeded()` already performs — an entered +capacity above the settled maximum comes down to it instead of the keypress being rejected. The +existing assertion ("does not exceed the maximum receiving capacity") holds either way, but three +behaviours it introduces are not covered yet and should be added to +`transfer-spending-advanced-over-max.xml` when it lands: + +- the number pad is disabled while the maximum settles +- an entered capacity above the settled maximum snaps down to it, with the toast showing the settled value +- tapping Max before the maximum settles brings the entered amount down once it does + ## Identifiers used - Number pad keys: digits `N0`–`N9`, triple-zero `N000`, decimal `NDecimal`, delete `NRemove`. - Send: screen `SendAmount`, field `SendNumberField`, available `AvailableAmount`, From 86f9b521bbf9c684d54b0d218af54a9245389d89 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:27:59 -0300 Subject: [PATCH 09/20] docs: add cross-platform journey guidance A journey is a shared spec, so a behaviour meant to match Android can be checked by running the same file on both sides. Documents the android CLI equivalents of the XcodeBuildMCP commands, and what to do when the two platforms disagree: record an intentional difference on both sides, or report a divergence as the bug it is, rather than rewriting the journey to match current behaviour. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 25 +++++++++++++++++++++++++ journeys/README.md | 7 +++++++ 2 files changed, 32 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ac907af32..e1bf17cea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -371,6 +371,31 @@ Ensure accessibility modifiers and labels are added to custom components. - Journeys are agent-evaluated and non-deterministic: run them from the manual `ai-device-tests` workflow, never as a blocking CI gate. +#### 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 start # or `android run` against a connected device +android layout # 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 `resourceId`, `text`, `contentDesc` and `interactions`, so a +journey's testTag assertions map onto it the way the iOS ones map onto `snapshot-ui` identifiers. + +When the two platforms disagree on a journey, decide which it is before touching anything: + +- an **intentional platform difference** — record it in the journey's `` and the suite + README, on both sides, so the next reader does not rediscover it; +- a **real divergence** — that is a bug on one platform, and the journey has done its job. Report it. + +Never quietly rewrite the iOS journey to match whatever iOS currently does; that turns a failing test +into a description of the bug. + ### Changelog - NEVER edit `CHANGELOG.md` in normal feature/fix PRs; release automation collects changelog fragments into it diff --git a/journeys/README.md b/journeys/README.md index fabd70136..23678134c 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -119,3 +119,10 @@ journeys are blocked on the feature existing, not on the harness. ## Porting from Android When you port an Android feature, port its journeys too — see the Journeys section in `AGENTS.md`. + +A journey is a shared spec, so a behaviour that is meant to match Android can be checked by running +the same file on both sides: `xcodebuildmcp` here, the `android` CLI against a `bitkit-android` +checkout there (`android layout` is the `snapshot-ui` equivalent). When the two disagree, either +record it as an intentional platform difference in the journey and this README, or report it as a +bug — do not rewrite the journey to match whatever the app currently does. `AGENTS.md` has the +commands. From 649d92e8b442697496e259a2ee2a83193a459928 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:34:27 -0300 Subject: [PATCH 10/20] fix: address review on disabled widget tiles and cjit runner docs Disabled widget tiles announced themselves as actionable buttons while their tap handler returns early, so VoiceOver offered a double-tap that did nothing. The button trait is now only added when the tile is enabled; the tile stays present by identifier, so journeys can still assert on it, but it is no longer an actionable target and WidgetEnableInSettings is the only control offered. The cjit-notifications suite requires a physical device but the README pointed at ai-device-tests.yml, which runs on the self-hosted macOS runner yet builds for platform=iOS Simulator, and gave a simctl log-stream command and a simulator snapshot step. Corrected to device commands, and the missing CI home is now stated rather than implied. Co-Authored-By: Claude Opus 5 (1M context) --- .../Views/Widgets/WidgetsListSheetView.swift | 4 +++- journeys/cjit-notifications/README.md | 19 +++++++++++-------- .../cjit-background-notification.xml | 2 +- .../cjit-push-single-notification.xml | 2 +- ...n-cjit-channel-no-payment-notification.xml | 2 +- journeys/widgets/README.md | 3 +++ 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Bitkit/Views/Widgets/WidgetsListSheetView.swift b/Bitkit/Views/Widgets/WidgetsListSheetView.swift index 85813c37c..73e1312f6 100644 --- a/Bitkit/Views/Widgets/WidgetsListSheetView.swift +++ b/Bitkit/Views/Widgets/WidgetsListSheetView.swift @@ -110,7 +110,9 @@ struct WidgetsListSheetView: View { navigationPath.append(.preview(type)) } .accessibilityElement(children: .combine) - .accessibilityAddTraits(.isButton) + // 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)") } diff --git a/journeys/cjit-notifications/README.md b/journeys/cjit-notifications/README.md index ec422f5e5..944d10a02 100644 --- a/journeys/cjit-notifications/README.md +++ b/journeys/cjit-notifications/README.md @@ -31,10 +31,12 @@ Consequences for the port: ## Preconditions - **A physical device, not the simulator.** These journeys need a real APNs round trip: the app must - hold a device token Blocktank can push to, and the NSE must decrypt a real Blocktank payload. - Run them on the self-hosted macOS runner used by `.github/workflows/ai-device-tests.yml`. + hold a device token Blocktank can push to, and the notification extension must decrypt a real + Blocktank payload. Note this suite has no CI home yet — `.github/workflows/ai-device-tests.yml` + runs on the self-hosted macOS runner but builds for `platform=iOS Simulator`, so it cannot host + these as written. Run them against a device attached to that runner, or add a device job first. `test-push-server/` can drive a hand-built push at a known device token when you need to exercise - the NSE without the LSP. + the extension without the LSP. - Onboarded regtest wallet connected to the LSP, with notifications authorized and "Get paid when Bitkit is closed" enabled in Settings → Notifications. - A funded CJIT entry ready to pay, and no Lightning channel open yet for the CJIT journeys. @@ -44,14 +46,15 @@ Consequences for the port: There is no `dumpsys notification`. Two options, in order of preference: 1. **Read the extension's own log.** `NotificationService` logs through `os_log`, unlike the main app - (which writes log files into the app group). Stream it while the push lands: + (which writes log files into the app group). Stream it from the Mac the device is attached to, + while the push lands: ```bash - xcrun simctl spawn log stream --predicate 'eventMessage CONTAINS "🔔"' # simulator - log stream --predicate 'eventMessage CONTAINS "🔔"' # attached device host + log stream --device --predicate 'eventMessage CONTAINS "🔔"' ``` The `🔔 Configured notification: type=…, title=…` line names the type and title it posted. -2. **Snapshot Notification Center.** Swipe down from the top of the screen and take a - `xcodebuildmcp simulator snapshot-ui`, then count the Bitkit entries and read their text. +2. **Read Notification Center on the device.** Swipe down from the top of the screen, then count the + Bitkit entries and read their text. `xcodebuildmcp simulator snapshot-ui` does not apply here — + it drives a simulator, and this suite runs on hardware. ## Identifiers used - In-app toast: `SpendingBalanceReadyToast`. diff --git a/journeys/cjit-notifications/cjit-background-notification.xml b/journeys/cjit-notifications/cjit-background-notification.xml index 220cbd482..bda95e951 100644 --- a/journeys/cjit-notifications/cjit-background-notification.xml +++ b/journeys/cjit-notifications/cjit-background-notification.xml @@ -15,7 +15,7 @@ Confirm "Get paid when Bitkit is closed" is enabled in Settings → Notifications, then go back to the wallet home screen Send the app to the background by pressing Home, leaving no Bitkit screen in the foreground - Start streaming the extension log: log stream --predicate 'eventMessage CONTAINS "🔔"' + Start streaming the extension log from the attached Mac: log stream --device --predicate 'eventMessage CONTAINS "🔔"' Pay the CJIT invoice via the regtest backend: ./lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' Wait up to ~30s for the push to arrive and the notification to be delivered Verify EXACTLY ONE Bitkit notification is posted for this channel opening, not two diff --git a/journeys/cjit-notifications/cjit-push-single-notification.xml b/journeys/cjit-notifications/cjit-push-single-notification.xml index d66747ef5..871c1ce23 100644 --- a/journeys/cjit-notifications/cjit-push-single-notification.xml +++ b/journeys/cjit-notifications/cjit-push-single-notification.xml @@ -12,7 +12,7 @@ Force-quit Bitkit from the app switcher so no Bitkit process is running - Start streaming the extension log: log stream --predicate 'eventMessage CONTAINS "🔔"' + Start streaming the extension log from the attached Mac: log stream --device --predicate 'eventMessage CONTAINS "🔔"' Pay the CJIT invoice via the regtest backend: ./lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' Wait up to ~30s for the push to wake the extension and deliver the notification Verify EXACTLY ONE Bitkit notification is posted for this CJIT payment diff --git a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml index 4be979221..ab3728b98 100644 --- a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml +++ b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml @@ -11,7 +11,7 @@ Keep the app in the foreground on the wallet home screen - Start streaming the extension log: log stream --predicate 'eventMessage CONTAINS "🔔"' + Start streaming the extension log from the attached Mac: log stream --device --predicate 'eventMessage CONTAINS "🔔"' Open a regular spending channel via Transfer → Spending, funding some sats and confirming Mine confirmations if required: ./lsp POST /regtest/chain/mine '{"count":3}' Wait for the ChannelReady event (~5-10s) diff --git a/journeys/widgets/README.md b/journeys/widgets/README.md index 7aed53088..9faeac440 100644 --- a/journeys/widgets/README.md +++ b/journeys/widgets/README.md @@ -27,6 +27,9 @@ Both journeys were walked on an iPhone 17 simulator with no backend running: - **`WidgetsAdd` sits below the fold** on the home widgets page — the journey has to scroll before it can be tapped, even though `--identifier WidgetsAdd --predicate exists` passes without scrolling. - All six tiles fit on an iPhone 17 sheet without scrolling, so that step is conditional. +- With "Show Widgets" off the tiles stay present by identifier but drop the button trait, so they + are no longer offered as tappable targets and `WidgetEnableInSettings` is the only actionable + control — matching the fact that their tap handler returns early. - Widget tiles are `.onTapGesture` views, not buttons. They now carry `.accessibilityElement(children: .combine)` + `.accessibilityAddTraits(.isButton)` so each is a single tappable target in the runtime snapshot; without `.combine` the identifier fanned out to From fda9eb9d915655f389a506f943cd8afa4c08a2df Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:50:11 -0300 Subject: [PATCH 11/20] docs: correct android CLI commands in the cross-platform section Ran the documented commands against a live emulator. Two were wrong: `android emulator start` takes a required AVD name rather than defaulting, and `android layout` emits hyphenated JSON keys (`resource-id`, `content-desc`) rather than camelCase. Also records that the Settings screen agrees on nine identifiers across the two platforms, so a comparison run is mostly signal. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e1bf17cea..9fbb2ba56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -378,14 +378,21 @@ sides rather than reasoning about the difference. iOS uses the XcodeBuildMCP CLI the `android` CLI against a `bitkit-android` checkout, which the `android-cli` agent skill drives: ```bash -android emulator start # or `android run` against a connected device -android layout # flat JSON of on-screen elements — the snapshot-ui equivalent +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 `resourceId`, `text`, `contentDesc` and `interactions`, so a -journey's testTag assertions map onto it the way the iOS ones map onto `snapshot-ui` identifiers. +`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. When the two platforms disagree on a journey, decide which it is before touching anything: From fe0a4f0ff498ac4242c648a65fa619fcfd2ea6f9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 09:54:53 -0300 Subject: [PATCH 12/20] fix: make Language and Transaction Speed settings rows addressable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the same Settings screen on both platforms surfaced two rows that resolved to the identifier "Value" on iOS, where Android has LanguageSettings and TransactionSpeedSettings. "Value" is the hardcoded identifier on the row's right-hand label in SettingsRow, so several rows were competing for it and neither row could be targeted. The Language row had no identifier at all. Transaction Speed had one, but `.accessibilityElement(children: .contain)` stopped the NavigationLink being the addressable element and let the inner value label surface instead — every sibling row that works uses a plain identifier with no container modifier. Dropped the modifier on both and added the missing identifier. Verified on simulator: the rows now resolve as LanguageSettings and TransactionSpeedSettings, matching Android. Co-Authored-By: Claude Opus 5 (1M context) --- Bitkit/Views/Settings/GeneralSettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bitkit/Views/Settings/GeneralSettingsView.swift b/Bitkit/Views/Settings/GeneralSettingsView.swift index aadcf6982..e6d8a6142 100644 --- a/Bitkit/Views/Settings/GeneralSettingsView.swift +++ b/Bitkit/Views/Settings/GeneralSettingsView.swift @@ -48,6 +48,7 @@ struct GeneralSettingsView: View { rightText: languageManager.currentLanguageDisplayName ) } + .accessibilityIdentifier("LanguageSettings") NavigationLink(value: Route.currencySettings) { SettingsRow( @@ -98,7 +99,6 @@ struct GeneralSettingsView: View { rightText: settings.defaultTransactionSpeed.title ) } - .accessibilityElement(children: .contain) .accessibilityIdentifier("TransactionSpeedSettings") if isPaykitUIActive, pubkyProfile.isAuthenticated { From 3aa006ecb5b2c4f8c6758ca1cd0354f741712520 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 10:00:49 -0300 Subject: [PATCH 13/20] docs: record Android input findings from a cross-platform payment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Paid a Lightning invoice generated on the iOS simulator from the Android emulator, end to end against the staging regtest backend. Two things worth writing down: `adb shell input text` drops characters — it lost 54 of a 397-character invoice — and `adb shell cmd clipboard` is not implemented on the emulator image. The reliable route is handing the app a lightning: URI via `am start`, which also skips the recipient screen. Also corrects the identifier table: Android emits both AvailableAmount and available_balance on the send amount screen, not only the latter. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 12 ++++++++++++ journeys/README.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 9fbb2ba56..f65007b55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -394,6 +394,18 @@ simulator, agrees on `Tab-general`, `Tab-security`, `Tab-advanced`, `NavigationB `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 ` 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:" to.bitkit.dev +``` + +A cross-platform payment is the sharpest check the two builds agree: take an invoice from one side +(`xcrun simctl pbpaste ` after tapping Copy on iOS) and pay it from the other. + When the two platforms disagree on a journey, decide which it is before touching anything: - an **intentional platform difference** — record it in the journey's `` and the suite diff --git a/journeys/README.md b/journeys/README.md index 23678134c..1f902cd08 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -91,7 +91,7 @@ Known naming differences: | Concept | Android testTag | iOS accessibilityIdentifier | | --- | --- | --- | | Send amount screen | `send_amount_screen` | `SendAmount` | -| Send available balance | `available_balance` | `AvailableAmount` | +| Send available balance | `AvailableAmount` and `available_balance` (Android emits both) | `AvailableAmount` | | Send max | `SendAmountMax` | *(no button — tap `AvailableAmount`)* | | External amount available | — | `ExternalAmountAvailable` | From 97cef747247b41533ca2bb33d474af0fe8a1b1f9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Aug 2026 12:49:36 -0300 Subject: [PATCH 14/20] fix: point the hardware activity tag journey at the right controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Traced against ActivityItemView and ActivityListFilter: ActivityTag is the Add Tag button and ActivityTags is the detail screen's chip list, so the chip assertion could pass merely because the Add Tag button was still on screen, and the All Activity filter lookup would miss entirely — that control is TagsPrompt, same as Android. Add Tag now names ActivityTag, the chip assertion names ActivityTags, and the filter names TagsPrompt. Suite README updated to spell out the distinction. Reported by @ovitrif in review. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/hardware-wallet/README.md | 3 ++- journeys/hardware-wallet/activity-detail-hw-tags.xml | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index 39a0e8533..1c0e7d704 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -113,4 +113,5 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities `HardwareTransferOpenTrezorConnect`, `HardwareTransferSigned`, `HwTransferPassphraseSheet`/`Input`/`Continue`/`Cancel`. - Activity: `ActivityShowAll`, `ActivityShort-`, tabs `Tab-all`/`Tab-sent`/`Tab-received`/`Tab-other`, - `ActivityTags`, `ActivityTag`, `ActivityTxDetails`. + Add Tag button `ActivityTag`, detail chip list `ActivityTags`, All Activity tag filter + `TagsPrompt` (not `ActivityTags` — that one is detail-screen only), explorer `ActivityTxDetails`. diff --git a/journeys/hardware-wallet/activity-detail-hw-tags.xml b/journeys/hardware-wallet/activity-detail-hw-tags.xml index 6cf236be3..26345ae06 100644 --- a/journeys/hardware-wallet/activity-detail-hw-tags.xml +++ b/journeys/hardware-wallet/activity-detail-hw-tags.xml @@ -9,12 +9,12 @@ Tap "Show All" (id "ActivityShowAll") beneath the activity list Tap a blue hardware received row that is not the Transfer row Verify the activity detail screen shows a blue icon and the selected received amount - Tap "Add Tag", enter "hwtest", and confirm the tag - Verify a tag chip labelled "hwtest" is shown (id "ActivityTag") + Tap "Add Tag" (id "ActivityTag"), enter "hwtest", and confirm the tag + Verify a tag chip labelled "hwtest" is shown in the detail screen's chip list (id "ActivityTags") Navigate back, reopen the same blue hardware row, and verify the "hwtest" tag is still shown Tap "Explore" (id "ActivityTxDetails") Verify the Activity Explorer screen shows an Inputs section and an Outputs section, each with at least one entry - Navigate back to All Activity, open the tag filter (id "ActivityTags"), and select "hwtest" + Navigate back to All Activity, open the tag filter (id "TagsPrompt"), and select "hwtest" Verify the tagged blue hardware activity remains listed and the other untagged hardware rows are absent From 77ba525d5479e34821a89cf93601954dc26edff7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Aug 2026 07:28:07 -0300 Subject: [PATCH 15/20] docs: reframe journeys as developer assistance and fix harness errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframing, per @ovitrif: drop the claim that a journey is the source of truth and that any disagreement means the app failed. Android carries no such rule, and the corpus is new enough here that a drift is far more likely to be a stale spec than a bug. Journeys are now described as agent-driven developer-assistance specs — they gate nothing, and a disagreement is a prompt to look rather than a bug report. Also removes the claim that journeys run on ai-device-tests.yml: that workflow runs TrezorBridgeDashboardUITests and never reads journeys/. Harness errors, per @ben-kaufman, each verified against the source: - The passphrase leak check used `grep ... || echo NO_PASSPHRASE_LEAK`, which prints the clean result for every nonzero status — a missing container or an unresolved simulator passed a security check without scanning anything. Now resolves the simulator explicitly and treats only grep status 1 as clean, reporting LEAK_CHECK_INVALID otherwise. Inherited from Android; noted as such. - passphrase-settings-remove counted files in a trezor-thp-credentials directory that does not exist. TrezorCredentialStorage keeps credentials as Keychain items under service to.bitkit.trezor.thp, so the step now validates the surviving pairing through observable behaviour. - The non-CJIT foreground journey asserted an extension log line that cannot be produced: NotificationService returns on the .lightning process lock while the node is running, before it decrypts or logs anything. - passphrase-duplicate expected an error toast; HwPassphraseView renders the failure inline as HwPassphraseError. - The cjit log-stream command used a --device flag /usr/bin/log does not have, and omitted that the line is logged at .info. Notification Center is now the supported assertion path. - The ./lsp funding commands are a bitkit-android helper, not something bitkit-docker ships, so they cannot run here as written. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 31 ++++++------- journeys/README.md | 43 +++++++++++-------- journeys/amount-limits/README.md | 6 +-- journeys/cjit-notifications/README.md | 27 ++++++------ ...n-cjit-channel-no-payment-notification.xml | 7 ++- journeys/hardware-wallet/README.md | 11 ++++- .../hardware-wallet/passphrase-duplicate.xml | 2 +- .../hardware-wallet/passphrase-pairing.xml | 6 +-- .../passphrase-settings-remove.xml | 3 +- .../passphrase-transfer-to-spending.xml | 4 +- 10 files changed, 77 insertions(+), 63 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f65007b55..0e77a0169 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,8 +147,6 @@ node scripts/validate-translations.js ### Testing -Three layers, in increasing order of setup cost: - ```bash # Unit and UI tests — Xcode Test Navigator, Cmd+U, or: xcodebuildmcp simulator test @@ -156,11 +154,11 @@ xcodebuildmcp simulator test # AI device tests (Trezor emulator, developer-triggered) — see Docs/AI_DEVICE_TESTS.md ``` -**Journeys** are the third layer: XML behaviour specs in `journeys/`, evaluated by an agent driving a -running simulator rather than by a test runner. They cover flows that are impractical to assert in -XCUITest — number pad caps, notification permission, widget flows, hardware wallet pairing and -transfers. Read `journeys/README.md` before running or writing one, and see the Journeys section -under Code Style & Conventions for the rule on porting them alongside Android changes. +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 @@ -368,8 +366,11 @@ Ensure accessibility modifiers and labels are added to custom components. `` 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 agent-evaluated and non-deterministic: run them from the manual `ai-device-tests` - workflow, never as a blocking CI gate. +- 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 @@ -406,14 +407,10 @@ adb shell am start -a android.intent.action.VIEW -d "lightning:" to.bit A cross-platform payment is the sharpest check the two builds agree: take an invoice from one side (`xcrun simctl pbpaste ` after tapping Copy on iOS) and pay it from the other. -When the two platforms disagree on a journey, decide which it is before touching anything: - -- an **intentional platform difference** — record it in the journey's `` and the suite - README, on both sides, so the next reader does not rediscover it; -- a **real divergence** — that is a bug on one platform, and the journey has done its job. Report it. - -Never quietly rewrite the iOS journey to match whatever iOS currently does; that turns a failing test -into a description of the bug. +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 `` 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 diff --git a/journeys/README.md b/journeys/README.md index 1f902cd08..c9a712ab9 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -1,15 +1,23 @@ # Journeys -A journey is an XML-specified test of the app's behaviour, evaluated by an agent driving a running -simulator. It is the source of truth: if the app disagrees with the journey, the app has failed. +A journey is an XML-specified walkthrough of app behaviour, evaluated by an agent driving a running +simulator. They are developer-assistance specs: they give an agent a reliable route through a flow so +it can reproduce a bug, check a change by hand, or show you what a screen does today. These are ported from [`bitkit-android/journeys`](https://github.com/synonymdev/bitkit-android/tree/main/journeys) and deliberately keep the same file names, journey names and `` prose so the two platforms stay diffable. Only the platform mechanics differ — `adb` becomes `xcodebuildmcp`, and Android `testTag`s become iOS `accessibilityIdentifier`s (the vocabulary is shared; see [Identifiers](#identifiers)). -Journeys are agent-evaluated, not deterministic. They belong on the manual `ai-device-tests` -workflow next to the XCUITest suites, **not** on a blocking CI gate. +**Journeys are not a QA gate.** They are agent-evaluated and non-deterministic, nothing runs them in +CI, and there is no runner wired up for them yet — `ai-device-tests.yml` runs `TrezorBridgeDashboardUITests` +and does not read `journeys/`. Treat a journey as a well-written description of a flow, not as an +authority on what the app owes you. + +A journey that no longer matches the app is most likely **stale**, not evidence of a bug. The corpus +is new on iOS and has not been run end to end, so when the two disagree the first assumption should be +that the spec drifted. Say what you found, update the journey, and only escalate when you have +separately confirmed the app is wrong. ## Format @@ -23,13 +31,14 @@ workflow next to the XCUITest suites, **not** on a blocking CI gate. ``` -- Evaluate `` elements in order. The journey passes only if every action passes. +- Evaluate `` elements in order, and report each one. An action that does not hold is worth + reporting as-is — it may be a stale step as easily as a real problem. - An action beginning with "check" or "verify" is an expectation about the **current** screen — inspect it, do not scroll or interact to satisfy it. - An action that specifies several interactions is split into sub-actions and evaluated individually. -- If an interaction cannot be performed as written, the journey fails. Report the failure; do not - debug around it. -- If the app crashes, exits or freezes, evaluation stops and the journey fails. +- If an interaction cannot be performed as written, say so and stop rather than improvising a route + around it — the point is to find out where the written route stopped matching the app. +- If the app crashes, exits or freezes, evaluation stops there. That one *is* worth escalating. ## Running a journey @@ -73,12 +82,13 @@ xcodebuildmcp simulator build-and-run \ ``` Fund a wallet before any amount journey — with a zero balance the caps fall back to the global -maximum and the journeys pass for the wrong reason: +maximum and the journeys pass for the wrong reason. -```bash -./lsp POST /regtest/chain/deposit '{"address":"","amountSat":100000}' -./lsp POST /regtest/chain/mine '{"count":3}' -``` +The `./lsp` helper the Android journeys call is a script in the **bitkit-android** repo root; there is +no equivalent checked in here and `bitkit-docker` does not ship one, so the commands inherited from +those journeys will not run as written. Until an iOS helper exists, fund the wallet however you +normally do against regtest — `bitcoin-cli` through the `bitkit-docker` compose stack, or the +Android checkout's `./lsp` if you have one. ## Identifiers @@ -122,7 +132,6 @@ When you port an Android feature, port its journeys too — see the Journeys sec A journey is a shared spec, so a behaviour that is meant to match Android can be checked by running the same file on both sides: `xcodebuildmcp` here, the `android` CLI against a `bitkit-android` -checkout there (`android layout` is the `snapshot-ui` equivalent). When the two disagree, either -record it as an intentional platform difference in the journey and this README, or report it as a -bug — do not rewrite the journey to match whatever the app currently does. `AGENTS.md` has the -commands. +checkout there (`android layout` is the `snapshot-ui` equivalent). A disagreement is worth writing +down — as an intentional platform difference, or as something to look into — but it is not by itself +a bug report. `AGENTS.md` has the commands. diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index 269afb716..51c529a34 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -15,9 +15,9 @@ screens (Send, Transfer→Spending, Receiving capacity, External node). 1. **Fund a real, positive available balance first.** With `0` available, the cap falls back to the global maximum, so nothing gets blocked and the journeys silently pass for the wrong reason. - Get an on-chain (Savings) address from Receive → Show Details. - - Fund and mine via the regtest backend: - `./lsp POST /regtest/chain/deposit '{"address":"","amountSat":100000}'` - then `./lsp POST /regtest/chain/mine '{"count":3}'`, and wait for the balance to sync. + - Fund and mine against regtest, then wait for the balance to sync. Note the `./lsp` commands + inherited from the Android journeys will not run here — that helper lives in the bitkit-android + repo and is not checked in on this side (see the top-level README). 2. **Transfer/Spending and Receiving-capacity flows need the node connected to the LSP** so a real max can be quoted. On the Spending amount screen the max starts at `0` behind a spinner — wait for it to populate before typing. diff --git a/journeys/cjit-notifications/README.md b/journeys/cjit-notifications/README.md index 944d10a02..2a0e40b06 100644 --- a/journeys/cjit-notifications/README.md +++ b/journeys/cjit-notifications/README.md @@ -32,10 +32,9 @@ Consequences for the port: ## Preconditions - **A physical device, not the simulator.** These journeys need a real APNs round trip: the app must hold a device token Blocktank can push to, and the notification extension must decrypt a real - Blocktank payload. Note this suite has no CI home yet — `.github/workflows/ai-device-tests.yml` - runs on the self-hosted macOS runner but builds for `platform=iOS Simulator`, so it cannot host - these as written. Run them against a device attached to that runner, or add a device job first. - `test-push-server/` can drive a hand-built push at a known device token when you need to exercise + Blocktank payload. Nothing runs this suite automatically, and that is not a gap to + fill here — `ai-device-tests.yml` runs `TrezorBridgeDashboardUITests` on a simulator and is not a + journey runner. Run these by hand against an attached device. `test-push-server/` can drive a hand-built push at a known device token when you need to exercise the extension without the LSP. - Onboarded regtest wallet connected to the LSP, with notifications authorized and "Get paid when Bitkit is closed" enabled in Settings → Notifications. @@ -45,16 +44,16 @@ Consequences for the port: There is no `dumpsys notification`. Two options, in order of preference: -1. **Read the extension's own log.** `NotificationService` logs through `os_log`, unlike the main app - (which writes log files into the app group). Stream it from the Mac the device is attached to, - while the push lands: - ```bash - log stream --device --predicate 'eventMessage CONTAINS "🔔"' - ``` - The `🔔 Configured notification: type=…, title=…` line names the type and title it posted. -2. **Read Notification Center on the device.** Swipe down from the top of the screen, then count the - Bitkit entries and read their text. `xcodebuildmcp simulator snapshot-ui` does not apply here — - it drives a simulator, and this suite runs on hardware. +1. **Read Notification Center on the device.** Swipe down from the top of the screen, then count the + Bitkit entries and read their text. This is the supported assertion path. + `xcodebuildmcp simulator snapshot-ui` does not apply — it drives a simulator, and this suite runs + on hardware. +2. **The extension's own log, if you can get at it.** `NotificationService` logs through `os_log` + (unlike the main app, which writes log files into the app group), at `.info` level, so any + predicate needs `--level info` to see it — the `🔔 Configured notification: type=…, title=…` line + names the type and title it posted. Note `/usr/bin/log stream` has no `--device` flag, so there is + no verified one-liner for an attached device here; use Console.app with the device selected, or + treat this as unavailable and rely on Notification Center. ## Identifiers used - In-app toast: `SpendingBalanceReadyToast`. diff --git a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml index ab3728b98..55e5acf34 100644 --- a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml +++ b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml @@ -5,20 +5,23 @@ path is the orderPaymentConfirmed copy, "Spending Balance Ready". When the app is in the foreground the only feedback is the in-app "Spending balance ready" toast. + No extension log assertion here: with the app in the foreground the node holds the .lightning + process lock, and NotificationService returns on that lock before it decrypts or logs anything. + The observable evidence is the toast plus the absence of an "Incoming Payment" notification. + Precondition: physical device with an onboarded regtest wallet connected to the LSP and a positive on-chain balance to fund the transfer. No CJIT entry is involved — this is a manual Transfer channel opening (orderPaymentConfirmed), not a CJIT receive. Keep the app in the foreground on the wallet home screen - Start streaming the extension log from the attached Mac: log stream --device --predicate 'eventMessage CONTAINS "🔔"' Open a regular spending channel via Transfer → Spending, funding some sats and confirming Mine confirmations if required: ./lsp POST /regtest/chain/mine '{"count":3}' Wait for the ChannelReady event (~5-10s) Verify an in-app "Spending balance ready" toast appears (id "SpendingBalanceReadyToast") Open Notification Center and inspect the Bitkit entries Verify NO "Incoming Payment" notification was posted for this channel opening - Verify any notification posted for it uses the "Spending Balance Ready" copy, and that the streamed log shows type=orderPaymentConfirmed rather than cjitPaymentArrived + Verify any notification posted for it uses the "Spending Balance Ready" copy and not the "Incoming Payment" copy Verify there are not multiple duplicate channel-ready notifications stacked in Notification Center diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index 1c0e7d704..c902753ee 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -65,10 +65,17 @@ Unlike the notification extension, the app writes its logs as **files in the app not to `os_log`. Resolve the container and grep it: ```bash -GROUP=$(xcrun simctl get_app_container to.bitkit groups | awk '{print $2}') -grep -rl "bitkit-hidden" "$GROUP" || echo NO_PASSPHRASE_LEAK +GROUP=$(xcrun simctl get_app_container "$UDID" to.bitkit groups | awk '{print $2}') +[ -d "$GROUP" ] || { echo "LEAK_CHECK_INVALID: container not found"; exit 1; } +grep -rl "bitkit-hidden" "$GROUP"; s=$? +case $s in 1) echo NO_PASSPHRASE_LEAK;; 0) echo "LEAK FOUND";; *) echo "LEAK_CHECK_INVALID: grep exit $s";; esac ``` +`grep ... || echo NO_PASSPHRASE_LEAK` — the form the Android journeys use — prints the clean result +for *every* nonzero status, so a missing container or an unresolved simulator passes the security +check without scanning anything. Only exit status 1 means "no match"; resolve `$UDID` explicitly +rather than relying on `booted`. + ## Verified on simulator Walked as far as a simulator allows, against a wallet with two Trezor identities already paired: diff --git a/journeys/hardware-wallet/passphrase-duplicate.xml b/journeys/hardware-wallet/passphrase-duplicate.xml index 8efe574ae..06756681b 100644 --- a/journeys/hardware-wallet/passphrase-duplicate.xml +++ b/journeys/hardware-wallet/passphrase-duplicate.xml @@ -17,7 +17,7 @@ prompts once per address type and the call blocks until it is acknowledged. Either tap the emulator UI or run: ../bitkit-docker/scripts/trezor-emulator send-json '{"type":"emulator-press-yes","id":1}' - Verify an error toast reports the passphrase wallet is already being watched, and that the sheet stays on the Passphrase step (id "HardwareWalletPassphraseScreen") with an empty input + Verify an inline error (id "HwPassphraseError") reports the passphrase wallet is already being watched — it renders in the sheet, not as a toast — and that the sheet stays on the Passphrase step (id "HardwareWalletPassphraseScreen") with an empty input Dismiss the sheet and return to the wallet home screen Verify the number of hardware wallet tiles is unchanged from the count noted at the start diff --git a/journeys/hardware-wallet/passphrase-pairing.xml b/journeys/hardware-wallet/passphrase-pairing.xml index d27d7d51c..0d6755f33 100644 --- a/journeys/hardware-wallet/passphrase-pairing.xml +++ b/journeys/hardware-wallet/passphrase-pairing.xml @@ -26,9 +26,9 @@ Navigate to the wallet home screen and verify two hardware wallet tiles are shown beneath the SAVINGS and SPENDING tiles, one of them labelled "Hidden Trezor" Verify the headline total balance is at least the sum of both hardware tile balances Tap the "Hidden Trezor" tile and verify its hardware wallet detail screen opens (id "HardwareWalletScreen") titled "Hidden Trezor" - Resolve the app group container and grep it for the passphrase: GROUP=$(xcrun simctl get_app_container booted to.bitkit groups | awk '{print $2}'); grep -rl "bitkit-hidden" "$GROUP" || echo NO_PASSPHRASE_LEAK - Verify the previous command printed NO_PASSPHRASE_LEAK: the passphrase must never reach the app logs or the app group storage, only the device session - Grep the newest app log for errors: grep -iE "error|exception" "$(ls -t "$GROUP"/logs/*.log | head -1)" | tail -20 + Resolve the app group container against the simulator under test and scan it, treating only "no match" as clean: UDID=<the simulator this journey is running on>; GROUP=$(xcrun simctl get_app_container "$UDID" to.bitkit groups | awk '{print $2}'); [ -d "$GROUP" ] || { echo "LEAK_CHECK_INVALID: container not found"; exit 1; }; grep -rl "bitkit-hidden" "$GROUP"; s=$?; case $s in 1) echo NO_PASSPHRASE_LEAK;; 0) echo "LEAK FOUND";; *) echo "LEAK_CHECK_INVALID: grep exit $s";; esac + Verify the previous command printed NO_PASSPHRASE_LEAK. LEAK_CHECK_INVALID means the check did not scan anything and the journey cannot conclude either way — treat it as a failed step, not a pass + Grep the newest app log for errors: grep -iE "error|exception" "$(ls -t "$GROUP"/logs/*.log | head -1)" | tail -20 — if no log file matches, say so rather than treating the empty result as clean Verify the previous command reported no Trezor connect, session or watcher errors while the hidden wallet was paired diff --git a/journeys/hardware-wallet/passphrase-settings-remove.xml b/journeys/hardware-wallet/passphrase-settings-remove.xml index 7bf6b0cbb..2f28729fb 100644 --- a/journeys/hardware-wallet/passphrase-settings-remove.xml +++ b/journeys/hardware-wallet/passphrase-settings-remove.xml @@ -15,8 +15,7 @@ Tap the delete (trash) icon on the "Hidden Funds" row, and confirm "Remove" in the dialog (id "RemoveHwWalletDialog") Verify the Hardware Wallets screen now lists exactly one row, the standard wallet, still showing its balance Navigate to the wallet home screen and verify a single hardware wallet tile remains with a non-zero balance - Count the stored Trezor credentials: GROUP=$(xcrun simctl get_app_container booted to.bitkit groups | awk '{print $2}'); ls "$GROUP"/trezor-thp-credentials/ 2>/dev/null | wc -l - Verify the previous command reported at least one credential file: removing one identity must not unpair the physical device + Verify the physical device is still paired rather than re-paired: the remaining row keeps its green connection indicator, with no Searching or Found step and no PIN or pairing prompt. Trezor credentials live in the Keychain under service "to.bitkit.trezor.thp", not as files in the app group, so there is no directory to count here Tap the remaining hardware wallet tile and verify its detail screen opens and lists its activity, confirming the device was not re-paired diff --git a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml b/journeys/hardware-wallet/passphrase-transfer-to-spending.xml index 4c7e71a02..1050b871f 100644 --- a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml +++ b/journeys/hardware-wallet/passphrase-transfer-to-spending.xml @@ -30,7 +30,7 @@ Navigate to the wallet home screen and verify the hardware tile count is unchanged: the wallet the wrong passphrase opened must not be added Reopen the sign screen, tap "Open Trezor Connect", type the correct passphrase "bitkit-hidden" and tap "Continue" Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator, and verify the transaction signed screen appears (id "HardwareTransferSigned") - Grep the app group for both passphrases: GROUP=$(xcrun simctl get_app_container booted to.bitkit groups | awk '{print $2}'); grep -rl -e "bitkit-hidden" -e "not-the-one" "$GROUP" || echo NO_PASSPHRASE_LEAK - Verify the previous command printed NO_PASSPHRASE_LEAK: neither the correct nor the rejected passphrase may be written to the logs or the app group storage + Scan the app group for both passphrases, treating only "no match" as clean: UDID=<the simulator this journey is running on>; GROUP=$(xcrun simctl get_app_container "$UDID" to.bitkit groups | awk '{print $2}'); [ -d "$GROUP" ] || { echo "LEAK_CHECK_INVALID: container not found"; exit 1; }; grep -rl -e "bitkit-hidden" -e "not-the-one" "$GROUP"; s=$?; case $s in 1) echo NO_PASSPHRASE_LEAK;; 0) echo "LEAK FOUND";; *) echo "LEAK_CHECK_INVALID: grep exit $s";; esac + Verify the previous command printed NO_PASSPHRASE_LEAK. LEAK_CHECK_INVALID means the check did not scan anything and the journey cannot conclude either way — treat it as a failed step, not a pass From 84a5c034c4b56a7c5b2a46f926e2a45aa03cdbc9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Aug 2026 07:37:07 -0300 Subject: [PATCH 16/20] fix: correct journey steps found by re-running the corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An audit pass in the spirit of @ben-kaufman's review — cross-checking every identifier the journeys name against the app, running the reachable ones, and checking every shell command actually executes. Deterministic false failures found and fixed: - `xcodebuildmcp simulator stop` and `launch-app` were written bare; both require --bundle-id and exit with "Missing required argument" as written. - Two cjit journeys still carried `log stream --device` after the README was corrected. /usr/bin/log has no such flag, so the step could never run. - Four journeys still invoked `./lsp`, which is a bitkit-android helper and is not checked in here. - The activity tag button is labelled "Tag", not "Add Tag"; the field is TagInput and the submit is ActivityTagsSubmit. Verified by running the flow. Traps documented rather than worked around: - Activity rows use two id schemes: ActivityShort- in the home recent list, Activity- in All Activity and on the hardware wallet screen. The suite README conflated them. - Gesture-based controls never appear as snapshot targets — the All Activity tag filter TagsPrompt resolves by identifier but is absent from the target list, so a missing control needs an --identifier check before concluding it is gone. - SegmentedControl builds its identifier from localized text, so Tab-all is Tab-todas in Spanish. Journeys naming a Tab-* id assume an English device. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/README.md | 9 +++++++++ journeys/amount-limits/external-amount-over-max.xml | 5 +++-- .../cjit-notifications/cjit-background-notification.xml | 4 ++-- .../cjit-notifications/cjit-push-single-notification.xml | 4 ++-- .../non-cjit-channel-no-payment-notification.xml | 2 +- journeys/hardware-wallet/README.md | 7 +++++-- journeys/hardware-wallet/activity-detail-hw-tags.xml | 3 ++- .../hardware-wallet/passphrase-transfer-to-spending.xml | 4 ++-- .../hardware-wallet/transfer-to-spending-node-warmup.xml | 4 ++-- 9 files changed, 28 insertions(+), 14 deletions(-) diff --git a/journeys/README.md b/journeys/README.md index c9a712ab9..ebb510429 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -61,6 +61,15 @@ shape: `--predicate textContains --text "..."`, not `--text-contains`. Refresh the snapshot after navigation, scrolling, sheet changes, or any obvious layout change — `elementRef`s from a stale snapshot are not reusable. +**Some controls never appear as snapshot targets.** Anything built on `.onTapGesture` rather than a +`Button` — the All Activity tag filter (`TagsPrompt`) is one — resolves by identifier but is absent +from the target list. If a control the journey names is missing from `snapshot-ui`, check it with +`--identifier X --predicate exists` before concluding it is gone. + +**Identifiers built from localized text are English-only.** `SegmentedControl` derives its identifier +from the tab's display name, so `Tab-all` is `Tab-todas` when the app runs in Spanish. Journeys that +name a `Tab-*` identifier assume an English device. + Prefer `snapshot-ui` over screenshots: it names elements by their accessibility identifier, which is what the journeys assert on, and it avoids the image-size limits the Android runner has to work around. diff --git a/journeys/amount-limits/external-amount-over-max.xml b/journeys/amount-limits/external-amount-over-max.xml index 20e321959..c170765fa 100644 --- a/journeys/amount-limits/external-amount-over-max.xml +++ b/journeys/amount-limits/external-amount-over-max.xml @@ -4,8 +4,9 @@ "Spending Balance Maximum" warning toast, and still allows deleting digits while at the cap. Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a reachable - external Lightning node to peer with — the staging LSP node works (`./lsp GET /info` for its - id, host and port). Start on the wallet home screen. + external Lightning node to peer with — the staging LSP node works (its id, host + and port come from the Blocktank info endpoint; the ./lsp helper the Android journeys use is not + checked in here). Start on the wallet home screen. Do NOT assert that Continue is disabled at the over-max input: the input is capped (never left in an over-max state), so the capped value is valid and Continue stays enabled. diff --git a/journeys/cjit-notifications/cjit-background-notification.xml b/journeys/cjit-notifications/cjit-background-notification.xml index bda95e951..94399e6f2 100644 --- a/journeys/cjit-notifications/cjit-background-notification.xml +++ b/journeys/cjit-notifications/cjit-background-notification.xml @@ -15,8 +15,8 @@ Confirm "Get paid when Bitkit is closed" is enabled in Settings → Notifications, then go back to the wallet home screen Send the app to the background by pressing Home, leaving no Bitkit screen in the foreground - Start streaming the extension log from the attached Mac: log stream --device --predicate 'eventMessage CONTAINS "🔔"' - Pay the CJIT invoice via the regtest backend: ./lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' + Open Console.app with the device selected and filter on "🔔", including info-level messages — /usr/bin/log has no --device flag, so there is no verified one-liner here; if the log is not reachable, skip it and rely on Notification Center below + Pay the CJIT invoice against regtest however your setup allows — the ./lsp helper the Android journeys use lives in the bitkit-android repo and is not checked in here Wait up to ~30s for the push to arrive and the notification to be delivered Verify EXACTLY ONE Bitkit notification is posted for this channel opening, not two Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" diff --git a/journeys/cjit-notifications/cjit-push-single-notification.xml b/journeys/cjit-notifications/cjit-push-single-notification.xml index 871c1ce23..a99bc1414 100644 --- a/journeys/cjit-notifications/cjit-push-single-notification.xml +++ b/journeys/cjit-notifications/cjit-push-single-notification.xml @@ -12,8 +12,8 @@ Force-quit Bitkit from the app switcher so no Bitkit process is running - Start streaming the extension log from the attached Mac: log stream --device --predicate 'eventMessage CONTAINS "🔔"' - Pay the CJIT invoice via the regtest backend: ./lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' + Open Console.app with the device selected and filter on "🔔", including info-level messages — /usr/bin/log has no --device flag, so there is no verified one-liner here; if the log is not reachable, skip it and rely on Notification Center below + Pay the CJIT invoice against regtest however your setup allows — the ./lsp helper the Android journeys use lives in the bitkit-android repo and is not checked in here Wait up to ~30s for the push to wake the extension and deliver the notification Verify EXACTLY ONE Bitkit notification is posted for this CJIT payment Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" diff --git a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml index 55e5acf34..5512c1ddf 100644 --- a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml +++ b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml @@ -16,7 +16,7 @@ Keep the app in the foreground on the wallet home screen Open a regular spending channel via Transfer → Spending, funding some sats and confirming - Mine confirmations if required: ./lsp POST /regtest/chain/mine '{"count":3}' + Mine confirmations if required, against regtest — note ./lsp is a bitkit-android helper and is not available in this repo Wait for the ChannelReady event (~5-10s) Verify an in-app "Spending balance ready" toast appears (id "SpendingBalanceReadyToast") Open Notification Center and inspect the Bitkit entries diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index c902753ee..ca78ed6bf 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -119,6 +119,9 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities `HardwareTransferSignLearnMore`/`SignAdvanced`/`SignDefault`, `HardwareTransferOpenTrezorConnect`, `HardwareTransferSigned`, `HwTransferPassphraseSheet`/`Input`/`Continue`/`Cancel`. -- Activity: `ActivityShowAll`, `ActivityShort-`, tabs `Tab-all`/`Tab-sent`/`Tab-received`/`Tab-other`, - Add Tag button `ActivityTag`, detail chip list `ActivityTags`, All Activity tag filter +- Activity: `ActivityShowAll` (home only). Rows use **two different schemes** — `ActivityShort-` + in the home recent list, `Activity-` in All Activity and on the hardware wallet screen. + Tabs `Tab-all`/`Tab-sent`/`Tab-received`/`Tab-other`, + Tag button `ActivityTag` (labelled "Tag"), tag field `TagInput`, submit `ActivityTagsSubmit`, + detail chip list `ActivityTags` (only rendered once a tag exists), All Activity tag filter `TagsPrompt` (not `ActivityTags` — that one is detail-screen only), explorer `ActivityTxDetails`. diff --git a/journeys/hardware-wallet/activity-detail-hw-tags.xml b/journeys/hardware-wallet/activity-detail-hw-tags.xml index 26345ae06..34908c9d4 100644 --- a/journeys/hardware-wallet/activity-detail-hw-tags.xml +++ b/journeys/hardware-wallet/activity-detail-hw-tags.xml @@ -9,7 +9,8 @@ Tap "Show All" (id "ActivityShowAll") beneath the activity list Tap a blue hardware received row that is not the Transfer row Verify the activity detail screen shows a blue icon and the selected received amount - Tap "Add Tag" (id "ActivityTag"), enter "hwtest", and confirm the tag + Tap the "Tag" button (id "ActivityTag") — it is labelled "Tag", not "Add Tag" + Type "hwtest" into the tag field (id "TagInput") and tap "Add" (id "ActivityTagsSubmit") Verify a tag chip labelled "hwtest" is shown in the detail screen's chip list (id "ActivityTags") Navigate back, reopen the same blue hardware row, and verify the "hwtest" tag is still shown Tap "Explore" (id "ActivityTxDetails") diff --git a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml b/journeys/hardware-wallet/passphrase-transfer-to-spending.xml index 1050b871f..17cf31923 100644 --- a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml +++ b/journeys/hardware-wallet/passphrase-transfer-to-spending.xml @@ -16,8 +16,8 @@ Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and verify no passphrase sheet appears, because the session opened at pairing still holds the hidden wallet Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator in order, and verify the transaction signed screen appears (id "HardwareTransferSigned") Wait for the Processing Payment screen, tap "Continue Using Bitkit", and verify the app returns to the wallet home screen - Force-quit Bitkit: xcodebuildmcp simulator stop - Relaunch it: xcodebuildmcp simulator launch-app + Force-quit Bitkit: xcodebuildmcp simulator stop --bundle-id to.bitkit + Relaunch it: xcodebuildmcp simulator launch-app --bundle-id to.bitkit Once the app is back on the home screen, open the hidden wallet tile and start Transfer To Spending again, setting an amount with the "25%" quick button and continuing to the sign screen Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and verify the passphrase sheet opens (id "HwTransferPassphraseSheet"), because the session that held the passphrase is gone Type a wrong passphrase "not-the-one" into the input (id "HwTransferPassphraseInput") and tap "Continue" (id "HwTransferPassphraseContinue") diff --git a/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml b/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml index 996a681f5..0d633ad6b 100644 --- a/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml +++ b/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml @@ -6,8 +6,8 @@ sign screen. Requires a paired and funded hardware wallet. - Force-quit Bitkit: xcodebuildmcp simulator stop - Relaunch it: xcodebuildmcp simulator launch-app + Force-quit Bitkit: xcodebuildmcp simulator stop --bundle-id to.bitkit + Relaunch it: xcodebuildmcp simulator launch-app --bundle-id to.bitkit As soon as the wallet home screen is visible, tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") Verify the transfer amount screen or loading/progress UI appears, and no reconnect, node-not-ready, or generic failure toast is shown while the node warms up From a28510a568c15a7d7719a1eda2dfbeac3095f8e2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Aug 2026 08:00:50 -0300 Subject: [PATCH 17/20] docs: borrow the Android lsp helper via the sibling checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interim fix suggested by @ovitrif on #694: rather than leaving the backend setup steps as prose, point them at ../bitkit-android/lsp. Most of us keep a clone of the sibling app at that path, and the hardware-wallet journeys already reach for ../bitkit-docker the same way. Restores the real commands in the amount-limits funding steps, the three cjit journeys, and the external-node peer lookup, each noting there is no iOS copy of the helper yet and pointing at #694 for the proper port. Not a permanent answer — the skill can drift if one platform updates it — but it beats a manual step the agent cannot perform. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/README.md | 17 +++++++++++------ journeys/amount-limits/README.md | 9 +++++---- .../amount-limits/external-amount-over-max.xml | 6 +++--- .../cjit-background-notification.xml | 2 +- .../cjit-push-single-notification.xml | 2 +- ...non-cjit-channel-no-payment-notification.xml | 2 +- 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/journeys/README.md b/journeys/README.md index ebb510429..417c2e822 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -91,13 +91,18 @@ xcodebuildmcp simulator build-and-run \ ``` Fund a wallet before any amount journey — with a zero balance the caps fall back to the global -maximum and the journeys pass for the wrong reason. +maximum and the journeys pass for the wrong reason: -The `./lsp` helper the Android journeys call is a script in the **bitkit-android** repo root; there is -no equivalent checked in here and `bitkit-docker` does not ship one, so the commands inherited from -those journeys will not run as written. Until an iOS helper exists, fund the wallet however you -normally do against regtest — `bitcoin-cli` through the `bitkit-docker` compose stack, or the -Android checkout's `./lsp` if you have one. +```bash +../bitkit-android/lsp POST /regtest/chain/deposit '{"address":"","amountSat":100000}' +../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}' +``` + +**The `lsp` helper is borrowed from the sibling Android checkout.** It is the `blocktank-api` plugin's +script, and there is no iOS copy yet — #694 tracks porting it. The relative path assumes +`bitkit-android` is cloned next to this repo, which is the usual layout here; the hardware-wallet +journeys already reach for `../bitkit-docker` the same way. If you do not have that clone, fund the +wallet however you normally do against regtest and skip the helper. ## Identifiers diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index 51c529a34..943341cd7 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -15,14 +15,15 @@ screens (Send, Transfer→Spending, Receiving capacity, External node). 1. **Fund a real, positive available balance first.** With `0` available, the cap falls back to the global maximum, so nothing gets blocked and the journeys silently pass for the wrong reason. - Get an on-chain (Savings) address from Receive → Show Details. - - Fund and mine against regtest, then wait for the balance to sync. Note the `./lsp` commands - inherited from the Android journeys will not run here — that helper lives in the bitkit-android - repo and is not checked in on this side (see the top-level README). + - Fund and mine via the sibling Android checkout's helper, then wait for the balance to sync: + `../bitkit-android/lsp POST /regtest/chain/deposit '{"address":"","amountSat":100000}'` + then `../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}'`. There is no iOS copy of + that helper yet — see #694 and the top-level README. 2. **Transfer/Spending and Receiving-capacity flows need the node connected to the LSP** so a real max can be quoted. On the Spending amount screen the max starts at `0` behind a spinner — wait for it to populate before typing. 3. **The external-node flow needs a reachable Lightning peer.** The staging LSP node works - (`./lsp GET /info` for its id/host/port). + (`../bitkit-android/lsp GET /info` for its id/host/port). ## Gotchas - **The cap can be lower than the visible "Available".** Fee and channel reserves mean e.g. Available diff --git a/journeys/amount-limits/external-amount-over-max.xml b/journeys/amount-limits/external-amount-over-max.xml index c170765fa..aa3b62239 100644 --- a/journeys/amount-limits/external-amount-over-max.xml +++ b/journeys/amount-limits/external-amount-over-max.xml @@ -4,9 +4,9 @@ "Spending Balance Maximum" warning toast, and still allows deleting digits while at the cap. Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a reachable - external Lightning node to peer with — the staging LSP node works (its id, host - and port come from the Blocktank info endpoint; the ./lsp helper the Android journeys use is not - checked in here). Start on the wallet home screen. + external Lightning node to peer with — the staging LSP node works — take its id, host and + port from `../bitkit-android/lsp GET /info`, borrowed from the sibling Android checkout because + there is no iOS copy of that helper yet (see #694). Start on the wallet home screen. Do NOT assert that Continue is disabled at the over-max input: the input is capped (never left in an over-max state), so the capped value is valid and Continue stays enabled. diff --git a/journeys/cjit-notifications/cjit-background-notification.xml b/journeys/cjit-notifications/cjit-background-notification.xml index 94399e6f2..81a644398 100644 --- a/journeys/cjit-notifications/cjit-background-notification.xml +++ b/journeys/cjit-notifications/cjit-background-notification.xml @@ -16,7 +16,7 @@ Confirm "Get paid when Bitkit is closed" is enabled in Settings → Notifications, then go back to the wallet home screen Send the app to the background by pressing Home, leaving no Bitkit screen in the foreground Open Console.app with the device selected and filter on "🔔", including info-level messages — /usr/bin/log has no --device flag, so there is no verified one-liner here; if the log is not reachable, skip it and rely on Notification Center below - Pay the CJIT invoice against regtest however your setup allows — the ./lsp helper the Android journeys use lives in the bitkit-android repo and is not checked in here + Pay the CJIT invoice using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' Wait up to ~30s for the push to arrive and the notification to be delivered Verify EXACTLY ONE Bitkit notification is posted for this channel opening, not two Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" diff --git a/journeys/cjit-notifications/cjit-push-single-notification.xml b/journeys/cjit-notifications/cjit-push-single-notification.xml index a99bc1414..eff6be393 100644 --- a/journeys/cjit-notifications/cjit-push-single-notification.xml +++ b/journeys/cjit-notifications/cjit-push-single-notification.xml @@ -13,7 +13,7 @@ Force-quit Bitkit from the app switcher so no Bitkit process is running Open Console.app with the device selected and filter on "🔔", including info-level messages — /usr/bin/log has no --device flag, so there is no verified one-liner here; if the log is not reachable, skip it and rely on Notification Center below - Pay the CJIT invoice against regtest however your setup allows — the ./lsp helper the Android journeys use lives in the bitkit-android repo and is not checked in here + Pay the CJIT invoice using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' Wait up to ~30s for the push to wake the extension and deliver the notification Verify EXACTLY ONE Bitkit notification is posted for this CJIT payment Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" diff --git a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml index 5512c1ddf..619313922 100644 --- a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml +++ b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml @@ -16,7 +16,7 @@ Keep the app in the foreground on the wallet home screen Open a regular spending channel via Transfer → Spending, funding some sats and confirming - Mine confirmations if required, against regtest — note ./lsp is a bitkit-android helper and is not available in this repo + Mine confirmations if required, using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}' Wait for the ChannelReady event (~5-10s) Verify an in-app "Spending balance ready" toast appears (id "SpendingBalanceReadyToast") Open Notification Center and inspect the Bitkit entries From 4995e3381bdefac8f1db41dbcb4807e2f9e08b2a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Aug 2026 08:26:35 -0300 Subject: [PATCH 18/20] docs: note that lsp mine is silent on success Ran the borrowed helper end to end against staging regtest: GET /info returns the node the external-node journey references, and deposit + mine funded the simulator wallet from 297 623 to 397 623 sats. deposit prints a txid but mine prints nothing and signals only through its exit status, which reads like a failure if you are not expecting it. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/README.md | 4 ++++ journeys/amount-limits/README.md | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/journeys/README.md b/journeys/README.md index 417c2e822..ad893ce79 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -98,6 +98,10 @@ maximum and the journeys pass for the wrong reason: ../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}' ``` +`deposit` prints the funding txid; **`mine` prints nothing on success** and signals only through its +exit status, so an empty response there is not a failure. Give the wallet ~20s to sync before +reading the balance. + **The `lsp` helper is borrowed from the sibling Android checkout.** It is the `blocktank-api` plugin's script, and there is no iOS copy yet — #694 tracks porting it. The relative path assumes `bitkit-android` is cloned next to this repo, which is the usual layout here; the hardware-wallet diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index 943341cd7..586765dfc 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -17,8 +17,9 @@ screens (Send, Transfer→Spending, Receiving capacity, External node). - Get an on-chain (Savings) address from Receive → Show Details. - Fund and mine via the sibling Android checkout's helper, then wait for the balance to sync: `../bitkit-android/lsp POST /regtest/chain/deposit '{"address":"","amountSat":100000}'` - then `../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}'`. There is no iOS copy of - that helper yet — see #694 and the top-level README. + then `../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}'`. `mine` prints nothing on + success — that is not a failure. There is no iOS copy of that helper yet — see #694 and the + top-level README. 2. **Transfer/Spending and Receiving-capacity flows need the node connected to the LSP** so a real max can be quoted. On the Spending amount screen the max starts at `0` behind a spinner — wait for it to populate before typing. From ad32815f6ead10b61b25bb01203ae45238859c67 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Aug 2026 08:48:19 -0300 Subject: [PATCH 19/20] docs: add XcodeBuildMCP setup to the README Short Development subsection covering install, `xcodebuildmcp setup`, and build-and-run, with the two things that bite here: the config is gitignored because it holds a machine-local simulator UDID, and there is no standalone .xcworkspace so it is --project-path. Points at AGENTS.md for the rest. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index b8f9d99d4..5f0aee648 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,29 @@ This installs a pre-commit hook that lints Swift files with SwiftFormat. Due to the Rust dependencies in the project, Xcode previews are only compatible with iOS 17 and below. +### XcodeBuildMCP + +Builds, simulator control and UI automation go through the [XcodeBuildMCP](https://github.com/getsentry/XcodeBuildMCP) CLI rather than raw `xcodebuild`, `xcrun` and `simctl`. It is also what agents use to run the specs in `journeys/`. + +**Install:** +```bash +brew tap getsentry/xcodebuildmcp && brew install xcodebuildmcp +``` + +**Configure project defaults:** +```bash +xcodebuildmcp setup +``` + +The wizard writes `.xcodebuildmcp/config.yaml`, which is gitignored because the CLI materializes a machine-local simulator UDID into it. The values this repo wants are project `Bitkit.xcodeproj`, scheme `Bitkit`, configuration `Debug`, simulator `iPhone 17`. There is no standalone `.xcworkspace` here, so this is `--project-path`, never `--workspace-path`. + +**Build and run:** +```bash +xcodebuildmcp simulator build-and-run +``` + +See the Agent CLI section in `AGENTS.md` for E2E compilation flags, UI automation and the cross-platform commands. + ### Common Tasks Day-to-day commands live in the `Justfile`: From 479697174741b1f54fbb1217f8d1e01ad97c4893 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 31 Aug 2026 06:29:52 -0300 Subject: [PATCH 20/20] fix: correct the transfer toggle label and the widgets intro reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both reported by @ovitrif, verified against the source. SpendingConfirm binds lightning__spending_confirm__background_setup, which is "Set up in background". The journey and suite README quoted "Enable background setup" — the Receive CJIT copy, applied here by mistake — so a verify step matching the label would fail, and it broke parity with Android's transfer journey. The two Receive CJIT screens keep their own wording; the suite README now spells out that the label differs by screen and the switch identifiers are the stable thing to match on. The widgets README told an agent to reset hasSeenWidgetsIntro with `xcodebuildmcp simulator install`, which exits with "Missing required argument: app-path" and would not wipe UserDefaults even with one. Points at the uninstall-then-build-and-run recipe the notification-permission suite already documents. Co-Authored-By: Claude Opus 5 (1M context) --- journeys/README.md | 2 +- journeys/notification-permission/README.md | 13 +++++++++---- ...ransfer-spending-confirm-notification-toggle.xml | 10 +++++----- journeys/widgets/README.md | 6 ++++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/journeys/README.md b/journeys/README.md index ad893ce79..905af7d60 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -132,7 +132,7 @@ Everything else — `N0`–`N9`, `N000`, `NDecimal`, `NRemove`, `SpendingAmount* | --- | --- | --- | | [amount-limits](amount-limits) | 4 | Number pad caps on all four amount screens | | [widgets](widgets) | 2 | Widgets intro and add-widget flow | -| [notification-permission](notification-permission) | 4 | "Set up in background" toggles | +| [notification-permission](notification-permission) | 4 | Background-setup toggles | | [cjit-notifications](cjit-notifications) | 3 | Adapted — iOS notification copy differs from Android | | [hardware-wallet](hardware-wallet) | 15 | Trezor over Bridge; see `Docs/AI_DEVICE_TESTS.md` | diff --git a/journeys/notification-permission/README.md b/journeys/notification-permission/README.md index bdba5cbf3..6b7d18db1 100644 --- a/journeys/notification-permission/README.md +++ b/journeys/notification-permission/README.md @@ -1,9 +1,14 @@ # Notification-permission journeys -Verify that the "Enable background setup" toggle — shown on the Receive → CJIT confirm screen, the -Receive → CJIT liquidity screen and the Transfer → Spending confirm screen — drives the iOS -notification authorization prompt, and that the app's own notification settings own the route into -the system Settings app. +Verify that the background-setup toggle — shown on the Receive → CJIT confirm screen, the Receive → +CJIT liquidity screen and the Transfer → Spending confirm screen — drives the iOS notification +authorization prompt, and that the app's own notification settings own the route into the system +Settings app. + +**The label differs by screen.** The two Receive CJIT screens bind +`wallet__receive_background_setup_toggle` ("Enable background setup"); Transfer → Spending confirm +binds `lightning__spending_confirm__background_setup` ("Set up in background"), matching Android. +The switch identifiers are stable, so prefer those over the copy. ## iOS vs Android diff --git a/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml index 46703a7c9..86013c37b 100644 --- a/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml +++ b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml @@ -1,11 +1,11 @@ - Verifies that the "Enable background setup" toggle on the Transfer → Spending confirm screen + Verifies that the "Set up in background" toggle on the Transfer → Spending confirm screen launches the iOS notification authorization alert, and that allowing it checks the toggle. Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and the node connected to the LSP (so a real max can be quoted), notification authorization still - notDetermined (reinstall first — the alert is one-shot), and "Enable background setup" starting + notDetermined (reinstall first — the alert is one-shot), and "Set up in background" starting off. The spending max starts at 0 behind a spinner — wait for it to populate. Start on the wallet home screen. Enter via the Savings card, not the Spending card — the Spending screen only offers "Transfer From Savings" while the spending balance is zero. @@ -17,10 +17,10 @@ On the spending amount screen (id "SpendingAmount"), wait until the available/maximum amount (id "SpendingAmountAvailable") finishes loading and shows a positive value Enter an amount within the maximum using the number pad Tap Continue (id "SpendingAmountContinue") and wait for the spending confirm screen to appear - Verify the "Enable background setup" toggle (id "SpendingConfirmNotificationSwitch") is visible and off - Tap the "Enable background setup" toggle (id "SpendingConfirmNotificationSwitch") + Verify the "Set up in background" toggle (id "SpendingConfirmNotificationSwitch") is visible and off + Tap the "Set up in background" toggle (id "SpendingConfirmNotificationSwitch") Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") Tap "Allow" - Verify the "Enable background setup" toggle is now on + Verify the "Set up in background" toggle is now on diff --git a/journeys/widgets/README.md b/journeys/widgets/README.md index 9faeac440..31e5a2fc4 100644 --- a/journeys/widgets/README.md +++ b/journeys/widgets/README.md @@ -8,8 +8,10 @@ Cover the widgets intro (first run) and the add-widget flow reached from the wal in the list sheet render disabled and an "Enable in settings" button (id `WidgetEnableInSettings`) is shown instead, and both journeys fail on the first tap. - `widgets-intro.xml` needs the intro **unseen** and `add-widgets-flow.xml` needs it **seen**. The - flag is `hasSeenWidgetsIntro` in `UserDefaults`; reset it by reinstalling the app - (`xcodebuildmcp simulator install` over a fresh wallet) or by wiping the simulator's app data. + flag is `hasSeenWidgetsIntro` in `UserDefaults`. Reset it the way the notification-permission suite + does — `xcrun simctl uninstall to.bitkit` then `xcodebuildmcp simulator build-and-run`, + which also means re-onboarding the wallet. `xcodebuildmcp simulator install` will not do it: it + requires `--app-path`, and installing over the bundle leaves `UserDefaults` intact. ## iOS notes - The drawer menu row is `DrawerWidgets`. With the intro unseen it pushes the intro screen