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..0e77a0169 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 @@ -100,11 +146,20 @@ node scripts/validate-translations.js **Note:** Localization files are synced from Transifex using [bitkit-transifex-sync](https://github.com/synonymdev/bitkit-transifex-sync). ### Testing + ```bash -# Run tests via Xcode Test Navigator or: -# Cmd+U in Xcode +# Unit and UI tests — Xcode Test Navigator, Cmd+U, or: +xcodebuildmcp simulator test + +# AI device tests (Trezor emulator, developer-triggered) — see Docs/AI_DEVICE_TESTS.md ``` +Separately from the test suites, `journeys/` holds XML walkthroughs of app behaviour that an agent +evaluates by driving a running simulator — number pad caps, notification permission, widget flows, +hardware wallet pairing and transfers. They are developer assistance rather than a test layer: +nothing runs them in CI and they gate nothing. Read `journeys/README.md` before running or writing +one, and see the Journeys section under Code Style & Conventions. + ## Architecture ### SwiftUI Patterns (CRITICAL) @@ -206,6 +261,7 @@ While the project is transitioning away from traditional ViewModels, these still - `Extensions/`: Swift extensions for utilities and mock data - `Utilities/`: Helper utilities (Logger, Keychain, Crypto, Haptics, StateLocker) - `Models/`: Data models (Toast, ElectrumServer, NodeLifecycleState, etc.) +- `journeys/`: XML behaviour specs evaluated by an agent on a simulator (see `journeys/README.md`) - `Styles/`: Fonts and sheet styles ### Service Queue Pattern @@ -290,6 +346,72 @@ Ensure accessibility modifiers and labels are added to custom components. - Follow Apple's SwiftUI best practices - AVOID code comments on private functions, types, etc — PREFER self-documenting names that make intent obvious without explanation; only add a comment when the rationale is genuinely non-obvious (e.g. a workaround, an edge case, or a "why" the code itself can't convey) +### Journeys + +`journeys/` holds XML behaviour specs evaluated by an agent driving a simulator — the iOS port of +[`bitkit-android/journeys`](https://github.com/synonymdev/bitkit-android/tree/main/journeys). Read +`journeys/README.md` before running or writing one. + +- **PORT the journeys whenever you port an Android feature.** If the Android change ships or touches + a journey under `bitkit-android/journeys/`, the iOS PR carries the matching journey. A ported + feature without its journey is an incomplete port. +- KEEP the file name, `` 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 developer-assistance specs, not a QA gate. Nothing runs them in CI and no runner is + wired up for them; `ai-device-tests.yml` runs `TrezorBridgeDashboardUITests` and never reads + `journeys/`. An agent runs one on request. +- A journey that disagrees with the app is most likely stale rather than evidence of a bug. Say what + you found and update the journey; escalate only once you have separately confirmed the app is wrong. + +#### Running a journey on both platforms + +A journey is a shared spec, so when a behaviour is meant to match Android, run the same file on both +sides rather than reasoning about the difference. iOS uses the XcodeBuildMCP CLI above; Android uses +the `android` CLI against a `bitkit-android` checkout, which the `android-cli` agent skill drives: + +```bash +android emulator list # AVD names; `start` requires one, it has no default +android emulator start Pixel_9 # or `android run` against a connected device +android layout --pretty # flat JSON of on-screen elements — the snapshot-ui equivalent +android layout --diff # only what changed, to keep context small +android screen capture -o shot.png # secondary; use when layout hits a WebView or animation +``` + +`android layout` reports each element's `resource-id`, `text`, `content-desc` and `interactions` +(note the hyphens — the JSON keys are not camelCase), so a journey's testTag assertions map onto it +the way the iOS ones map onto `snapshot-ui` identifiers. + +The vocabulary really is shared. Settings on both platforms, captured from a live emulator and +simulator, agrees on `Tab-general`, `Tab-security`, `Tab-advanced`, `NavigationBack`, `HeaderMenu`, +`CurrenciesSettings`, `UnitSettings`, `WidgetsSettings` and `QuickpaySettings` — so a comparison run +is mostly signal, and the rows that disagree stand out. + +Driving Android input: taps are `adb shell input tap ` 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, 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 - 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/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 { 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 91d3a6d7e..cd2d79fda 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..73e1312f6 100644 --- a/Bitkit/Views/Widgets/WidgetsListSheetView.swift +++ b/Bitkit/Views/Widgets/WidgetsListSheetView.swift @@ -109,6 +109,10 @@ struct WidgetsListSheetView: View { guard enabled else { return } navigationPath.append(.preview(type)) } + .accessibilityElement(children: .combine) + // Tapping is gated on `enabled`, so a dimmed tile must not announce itself as + // actionable — VoiceOver would otherwise offer a double-tap that silently does nothing. + .accessibilityAddTraits(enabled ? AccessibilityTraits.isButton : []) .accessibilityIdentifier("WidgetListItem-\(type.rawValue)") } 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`: diff --git a/journeys/README.md b/journeys/README.md new file mode 100644 index 000000000..905af7d60 --- /dev/null +++ b/journeys/README.md @@ -0,0 +1,155 @@ +# Journeys + +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 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 + +```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, 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, 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 + +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. + +**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. + +## 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 +../bitkit-android/lsp POST /regtest/chain/deposit '{"address":"","amountSat":100000}' +../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 +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 + +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 | `AvailableAmount` and `available_balance` (Android emits both) | `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 | 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` | + +## 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`. + +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). 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 new file mode 100644 index 000000000..586765dfc --- /dev/null +++ b/journeys/amount-limits/README.md @@ -0,0 +1,86 @@ +# 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 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}'`. `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. +3. **The external-node flow needs a reachable Lightning peer.** The staging LSP node works + (`../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 + `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. + +## 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`, + 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..aa3b62239 --- /dev/null +++ b/journeys/amount-limits/external-amount-over-max.xml @@ -0,0 +1,29 @@ + + + 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 — 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. + + + 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 + + diff --git a/journeys/cjit-notifications/README.md b/journeys/cjit-notifications/README.md new file mode 100644 index 000000000..2a0e40b06 --- /dev/null +++ b/journeys/cjit-notifications/README.md @@ -0,0 +1,60 @@ +# 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 notification extension must decrypt a real + 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. +- 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 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`. +- 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..81a644398 --- /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 + 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 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" + 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..eff6be393 --- /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 + 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 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" + 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..619313922 --- /dev/null +++ b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml @@ -0,0 +1,27 @@ + + + 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. + + 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 + Open a regular spending channel via Transfer → Spending, funding some sats and confirming + 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 + Verify NO "Incoming Payment" notification was posted for this channel opening + 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 new file mode 100644 index 000000000..ca78ed6bf --- /dev/null +++ b/journeys/hardware-wallet/README.md @@ -0,0 +1,127 @@ +# 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 "$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: + +- **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` (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-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..34908c9d4 --- /dev/null +++ b/journeys/hardware-wallet/activity-detail-hw-tags.xml @@ -0,0 +1,21 @@ + + + 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 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") + 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 "TagsPrompt"), 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..06756681b --- /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 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 new file mode 100644 index 000000000..0d6755f33 --- /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 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 new file mode 100644 index 000000000..2f28729fb --- /dev/null +++ b/journeys/hardware-wallet/passphrase-settings-remove.xml @@ -0,0 +1,21 @@ + + + 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 + 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 new file mode 100644 index 000000000..17cf31923 --- /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 --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") + + 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") + 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 + + 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..0d633ad6b --- /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 --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 + 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" + + diff --git a/journeys/notification-permission/README.md b/journeys/notification-permission/README.md new file mode 100644 index 000000000..6b7d18db1 --- /dev/null +++ b/journeys/notification-permission/README.md @@ -0,0 +1,49 @@ +# Notification-permission journeys + +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 + +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..86013c37b --- /dev/null +++ b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml @@ -0,0 +1,26 @@ + + + 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 "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. + + + 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 "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 "Set up in background" toggle is now on + + diff --git a/journeys/widgets/README.md b/journeys/widgets/README.md new file mode 100644 index 000000000..31e5a2fc4 --- /dev/null +++ b/journeys/widgets/README.md @@ -0,0 +1,47 @@ +# 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 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 + (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. +- 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 + 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 + +