diff --git a/.agents/commands/pr.md b/.agents/commands/pr.md index 05f1cc9bf..11df5dfaf 100644 --- a/.agents/commands/pr.md +++ b/.agents/commands/pr.md @@ -35,6 +35,11 @@ If no base branch argument provided, detect the repo's default branch: - Use result as default (typically `main` or `master`) - If command fails, fall back to `master` +### 2.6. Regenerate the Journeys Index +- Run `python3 scripts/journeys_index.py` +- If `journeys/index.json` changed, commit it as `chore: update journeys index` +- If it reports an identifier that no source file declares, stop and report the journey and the identifier + ### 3. Gather Context - Get current branch name: `git branch --show-current` - Extract repo identifier: `git remote get-url origin | sed 's/\.git$//' | sed -E 's#.*[:/]([^/]+/[^/]+)$#\1#'` (e.g., `synonymdev/bitkit-ios`) @@ -42,6 +47,7 @@ If no base branch argument provided, detect the repo's default branch: - Fetch 10 most recent PRs (open or closed) from the extracted repo for writing style reference - Run `git log $base..HEAD --oneline` for commit messages - Run `git diff $base...HEAD --stat` for understanding scope of changes +- List the journeys the branch adds or updates: `git diff --name-only --diff-filter=d $base...HEAD -- journeys | grep '\.xml$'` ### 4. Extract Linked Issues Scan commits for issue references: @@ -127,9 +133,13 @@ When the user provides custom instructions after `--`: - Always use this structure: ```md ### QA Notes + #### Journeys #### Manual Tests #### Automated Checks ``` +- Under `#### Journeys`, list each journey the branch adds or updates (Step 3) as a list item with its repo path. Write `N/A — no user-visible behaviour change.` only when the diff changes no user-visible behaviour; when it does and the list is empty, stop and report the flows that need a journey. +- Stop and report any journey whose `sources` in `journeys/index.json` include a file the branch changes and whose route no longer matches the diff. +- Under `#### Manual Tests`, keep only what a journey cannot express, such as hardware, push notifications, or a companion app, and end each item with the reason. Write no manual test for a flow a listed journey covers. - Keep local verification commands, `xcodebuild`, Swift tests, SwiftFormat, translation validation, unit tests, build passes, cargo test, cargo clippy, npm test, typecheck, CI coverage, or similar automated checks out of `#### Manual Tests`; summarize them under `#### Automated Checks` when they add useful context. - Use `#### Automated Checks` to summarize automated verification evidence, prioritizing coverage added, modified, or removed with file paths and a short explanation. - For removed automated coverage, state why it was removed. @@ -140,7 +150,7 @@ When the user provides custom instructions after `--`: - If no automated checks were run and no automated coverage changed, write `N/A` under `#### Automated Checks`. - Write manual tests using this template: ```md - - [ ] **{numbering}.** {optional_condition + →} {screen_action} → {next_screen_action}: expectation + - [ ] **{numbering}.** {optional_condition + →} {screen_action} → {next_screen_action}: expectation — {reason a journey cannot express it} ``` - Use a list of unchecked checkboxes for each individual test. - Use a numbered prefix for each test, in bold, for example `**1.**`, `**2.**`. @@ -168,16 +178,13 @@ Example: Concrete style target: ```md ### QA Notes +#### Journeys +- `journeys/amount-limits/send-amount-over-balance.xml` +- `journeys/widgets/add-widgets-flow.xml` #### Manual Tests -- [ ] **1.** No usable channels/spending balance → scan LN invoice: error shows immediately, not after 15s. -- [ ] **2.** Scanner → scan fixed amount LN invoice: Send Confirm or QuickPay opens directly. -- [ ] **3a.** `regression:` Send → scanner/paste fixed amount LN invoice: in-sheet nav to Confirm or QuickPay. - - [ ] **3b.** `regression:` Variable amount LN invoice/LNURL-pay: lands on Amount view. -- [ ] **4a.** Activity Detail of LN transfer → tap Connection: lands on Channel Detail. - - [ ] **4b.** back: returns to Activity Detail. -- [ ] **5a.** Settings → Lightning Connections → tap channel: still opens Channel Detail. - - [ ] **5b.** back: returns to Connections List. -- [ ] **6.** `regression:` Channel Detail → tap Close Connection: works. +- [ ] **1a.** Physical device → Scanner → scan a printed fixed amount LN invoice QR: Send Confirm opens directly — needs a device camera. + - [ ] **1b.** `regression:` Printed LNURL-pay QR: lands on Amount view — needs a device camera. +- [ ] **2.** Pubky Ring installed → Profile → sign in with Pubky Ring: profile shows as connected — needs the Pubky Ring app. #### Automated Checks - Unit tests added: cover invoice timeout handling in `BitkitTests/TransferViewModelTests.swift`. - Unit tests modified: update channel navigation assertions in `BitkitTests/ChannelDetailsViewModelTests.swift`. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 526002e03..f15c14002 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,3 +21,17 @@ Add any links to GitHub issues or Asana tasks that are relevant to this pull req ### Screenshot / Video Insert relevant screenshot / recording + +### QA Notes + +#### Journeys + + + +#### Manual Tests + + + +#### Automated Checks + + diff --git a/.github/workflows/journeys.yml b/.github/workflows/journeys.yml new file mode 100644 index 000000000..7a4992f2b --- /dev/null +++ b/.github/workflows/journeys.yml @@ -0,0 +1,22 @@ +name: Journeys + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +jobs: + index: + name: Check journeys index + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Check journeys index + run: python3 scripts/journeys_index.py --check diff --git a/AGENTS.md b/AGENTS.md index 5398d5923..c096d8e6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,9 +156,9 @@ xcodebuildmcp simulator test 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. +hardware wallet pairing and transfers. They are the QA contract for PRs rather than a test suite: +CI does not run them, and it checks that `journeys/index.json` matches them. Read `journeys/README.md` +before running or writing one, and see the Journeys section under Code Style & Conventions. ## Architecture @@ -377,9 +377,13 @@ Ensure accessibility modifiers and labels are added to custom components. `` and the suite README — never assert Android behaviour iOS does not have. - SKIP a journey only when the iOS feature does not exist, and record it under "Not ported" in `journeys/README.md` with what is missing. -- Journeys are 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. +- ADD or UPDATE, in the same PR, the journeys that prove a user-visible behaviour change and every + journey whose route the PR changes; `journeys/index.json` lists the source files each journey's + identifiers are declared in. List those journeys under `#### Journeys` in the PR's QA Notes. +- NAME an identifier in a journey action as `id "Name"`. +- RUN `python3 scripts/journeys_index.py` after changing a journey or an identifier it names, and + commit `journeys/index.json`. CI fails on a stale index or an undeclared identifier; it does not run + the journeys themselves. - 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. diff --git a/journeys/README.md b/journeys/README.md index b506fa0a4..5dba22079 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -9,10 +9,8 @@ and deliberately keep the same file names, journey names and `` prose so 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. +**Journeys are the QA contract for PRs** (see the Journeys section in `AGENTS.md`). They are +agent-evaluated and non-deterministic, so CI does not run them; it only checks `journeys/index.json`. 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 diff --git a/journeys/index.json b/journeys/index.json new file mode 100644 index 000000000..b2a6abedd --- /dev/null +++ b/journeys/index.json @@ -0,0 +1,786 @@ +[ + { + "file": "journeys/amount-limits/external-amount-over-max.xml", + "name": "external node amount over max is blocked", + "identifiers": [ + "FundManual", + "NodeIdInput", + "HostInput", + "PortInput", + "ExternalContinue", + "ExternalAmount", + "ExternalAmountAvailable", + "N9", + "ExternalAmountExceededToast", + "ExternalAmountNumberField", + "NRemove" + ], + "sources": [ + "Bitkit/Components/NumberPad.swift", + "Bitkit/Views/Settings/Advanced/ElectrumSettingsScreen.swift", + "Bitkit/Views/Transfer/FundAdvancedOptions.swift", + "Bitkit/Views/Transfer/FundManualAmountView.swift", + "Bitkit/Views/Transfer/FundManualSetupView.swift", + "Bitkit/Views/Transfer/FundingOptions.swift" + ], + "screens": [ + "funding options screen", + "external node amount screen" + ] + }, + { + "file": "journeys/amount-limits/send-amount-over-balance.xml", + "name": "send amount over balance is blocked", + "identifiers": [ + "Send", + "RecipientManual", + "RecipientInput", + "AddressContinue", + "SendAmount", + "AvailableAmount", + "N9", + "SendAmountExceededToast", + "SendNumberField", + "NRemove" + ], + "sources": [ + "Bitkit/Components/NumberPad.swift", + "Bitkit/Components/TabBar/TabBar.swift", + "Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift", + "Bitkit/Views/Wallets/Send/LnurlPayAmount.swift", + "Bitkit/Views/Wallets/Send/SendAmountView.swift", + "Bitkit/Views/Wallets/Send/SendEnterManuallyView.swift", + "Bitkit/Views/Wallets/Send/SendOptionsView.swift" + ], + "screens": [ + "Send amount screen" + ] + }, + { + "file": "journeys/amount-limits/transfer-spending-advanced-over-max.xml", + "name": "receiving capacity amount over max is blocked", + "identifiers": [ + "ActivitySavings", + "TransferToSpending", + "SpendingAmount", + "SpendingAmountAvailable", + "SpendingAmountMax", + "SpendingAmountContinue", + "SpendingConfirmAdvanced", + "SpendingAdvanced", + "SpendingAdvancedMax", + "N9", + "SpendingAdvancedNumberField", + "SpendingAdvancedExceededToast", + "NRemove" + ], + "sources": [ + "Bitkit/Components/NumberPad.swift", + "Bitkit/Views/Home/HomeWalletView.swift", + "Bitkit/Views/Transfer/SpendingAdvancedView.swift", + "Bitkit/Views/Transfer/SpendingAmount.swift", + "Bitkit/Views/Transfer/SpendingConfirm.swift", + "Bitkit/Views/Wallets/SavingsWalletScreen.swift" + ], + "screens": [ + "home screen", + "transfer intro screen", + "spending amount screen", + "confirm screen", + "receiving capacity screen" + ] + }, + { + "file": "journeys/amount-limits/transfer-spending-over-max.xml", + "name": "transfer to spending amount over max is blocked", + "identifiers": [ + "ActivitySavings", + "TransferToSpending", + "SpendingAmount", + "SpendingAmountAvailable", + "N9", + "SpendingAmountExceededToast", + "SpendingAmountNumberField", + "NRemove" + ], + "sources": [ + "Bitkit/Components/NumberPad.swift", + "Bitkit/Views/Home/HomeWalletView.swift", + "Bitkit/Views/Transfer/SpendingAmount.swift", + "Bitkit/Views/Wallets/SavingsWalletScreen.swift" + ], + "screens": [ + "home screen", + "transfer intro screen", + "spending amount screen" + ] + }, + { + "file": "journeys/cjit-notifications/cjit-background-notification.xml", + "name": "cjit notification while backgrounded is single and correctly typed", + "identifiers": [], + "sources": [], + "screens": [ + "wallet home screen", + "received-transaction sheet" + ] + }, + { + "file": "journeys/cjit-notifications/cjit-push-single-notification.xml", + "name": "cjit push notification is delivered exactly once", + "identifiers": [], + "sources": [], + "screens": [] + }, + { + "file": "journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml", + "name": "non-cjit channel opening shows no payment notification", + "identifiers": [ + "SpendingBalanceReadyToast" + ], + "sources": [ + "Bitkit/ViewModels/AppViewModel.swift" + ], + "screens": [ + "wallet home screen" + ] + }, + { + "file": "journeys/hardware-wallet/activity-blue-icons.xml", + "name": "Hardware Wallet Activity Blue Icons", + "identifiers": [ + "ActivityShowAll", + "Tab-sent", + "Tab-received", + "Tab-other", + "Tab-all" + ], + "sources": [ + "Bitkit/Components/SegmentedControl.swift", + "Bitkit/Views/Wallets/Activity/ActivityLatest.swift" + ], + "screens": [ + "wallet home screen", + "detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/activity-detail-hw-tags.xml", + "name": "Hardware Wallet Activity Tags And Inputs Outputs", + "identifiers": [ + "ActivityShowAll", + "ActivityTag", + "TagInput", + "ActivityTagsSubmit", + "ActivityTags", + "ActivityTxDetails", + "TagsPrompt" + ], + "sources": [ + "Bitkit/Components/Activity/ActivityListFilter.swift", + "Bitkit/Views/Wallets/Activity/ActivityItemView.swift", + "Bitkit/Views/Wallets/Activity/ActivityLatest.swift", + "Bitkit/Views/Wallets/Sheets/AddTagSheet.swift" + ], + "screens": [ + "wallet home screen", + "activity detail screen", + "detail screen", + "Activity Explorer screen" + ] + }, + { + "file": "journeys/hardware-wallet/connect-flow.xml", + "name": "Hardware Wallet Connect Flow From Settings", + "identifiers": [ + "TrezorForgetDevice-bridge", + "Tab-general", + "HardwareWalletsSettings", + "HardwareWalletsScreen", + "AddHardwareWallet", + "HardwareWalletIntroScreen", + "HardwareWalletIntroContinue", + "HardwareWalletSearchingScreen", + "HardwareWalletFoundScreen", + "HardwareWalletFoundConnect", + "HardwareWalletPairedScreen", + "HardwareWalletLabelInput", + "HardwareWalletPairedFinish" + ], + "sources": [ + "Bitkit/Components/SegmentedControl.swift", + "Bitkit/Components/Trezor/TrezorDeviceRow.swift", + "Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift", + "Bitkit/Views/Settings/GeneralSettingsView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwFoundView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwPairedView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwSearchingView.swift", + "Bitkit/Views/Sheets/HardwareConnectSheet.swift" + ], + "screens": [ + "Hardware Wallets screen" + ] + }, + { + "file": "journeys/hardware-wallet/connect-home-tile.xml", + "name": "Hardware Wallet Connect and Home Tile", + "identifiers": [ + "DrawerSettings", + "TrezorScanButton", + "TrezorDevice-bridge", + "ActivityHardware", + "HardwareWalletScreen" + ], + "sources": [ + "Bitkit/Components/DrawerView.swift", + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Components/Trezor/TrezorDeviceRow.swift", + "Bitkit/Views/Trezor/TrezorDeviceListView.swift", + "Bitkit/Views/Wallets/HardwareWalletScreen.swift" + ], + "screens": [ + "wallet home screen", + "hardware wallet detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/detail-overview.xml", + "name": "Hardware Wallet Detail Overview", + "identifiers": [ + "ActivityHardware", + "HardwareWalletScreen", + "HardwareTransferToSpending", + "HardwareTransferAmount", + "RemoveHardwareWallet", + "RemoveHwWalletDialog" + ], + "sources": [ + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Components/RemoveHwWalletDialog.swift", + "Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift", + "Bitkit/Views/Wallets/HardwareWalletScreen.swift" + ], + "screens": [ + "wallet home screen", + "hardware wallet detail screen", + "transfer amount screen", + "activity detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/passphrase-duplicate.xml", + "name": "Hardware Wallet Passphrase Duplicate", + "identifiers": [ + "ActivityHardware", + "Tab-general", + "HardwareWalletsSettings", + "AddHardwareWallet", + "HardwareWalletIntroContinue", + "HardwareWalletFoundConnect", + "HardwareWalletPairedPassphrase", + "HardwareWalletPassphraseInput", + "HardwareWalletPassphraseContinue", + "HwPassphraseError", + "HardwareWalletPassphraseScreen" + ], + "sources": [ + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Components/SegmentedControl.swift", + "Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift", + "Bitkit/Views/Settings/GeneralSettingsView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwFoundView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwPairedView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwPassphraseView.swift", + "Bitkit/Views/Sheets/HardwareConnectSheet.swift" + ], + "screens": [ + "wallet home screen" + ] + }, + { + "file": "journeys/hardware-wallet/passphrase-pairing.xml", + "name": "Hardware Wallet Passphrase Pairing", + "identifiers": [ + "Tab-general", + "HardwareWalletsSettings", + "AddHardwareWallet", + "HardwareWalletIntroContinue", + "HardwareWalletFoundScreen", + "HardwareWalletFoundConnect", + "HardwareWalletPairedScreen", + "HardwareWalletPairedPassphrase", + "HardwareWalletPairedFinish", + "HardwareWalletPassphraseScreen", + "HardwareWalletPassphraseContinue", + "HardwareWalletPassphraseInput", + "HardwareWalletPassphrasePairedScreen", + "HardwareWalletLabelInput", + "HardwareWalletScreen" + ], + "sources": [ + "Bitkit/Components/SegmentedControl.swift", + "Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift", + "Bitkit/Views/Settings/GeneralSettingsView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwFoundView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwPairedView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwPassphraseView.swift", + "Bitkit/Views/Sheets/HardwareConnectSheet.swift", + "Bitkit/Views/Wallets/HardwareWalletScreen.swift" + ], + "screens": [ + "wallet home screen", + "hardware wallet detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/passphrase-settings-remove.xml", + "name": "Hardware Wallet Passphrase Settings And Remove", + "identifiers": [ + "DrawerSettings", + "Tab-general", + "HardwareWalletsSettings", + "HardwareWalletsScreen", + "RenameHardwareWalletInput", + "RenameHardwareWalletSave", + "RemoveHwWalletDialog" + ], + "sources": [ + "Bitkit/Components/DrawerView.swift", + "Bitkit/Components/RemoveHwWalletDialog.swift", + "Bitkit/Components/SegmentedControl.swift", + "Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift", + "Bitkit/Views/Settings/GeneralSettingsView.swift", + "Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift" + ], + "screens": [ + "rename sheet", + "Hardware Wallets screen", + "wallet home screen", + "detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/passphrase-transfer-to-spending.xml", + "name": "Hardware Wallet Passphrase Transfer To Spending", + "identifiers": [ + "HardwareWalletScreen", + "HardwareTransferToSpending", + "HardwareTransferAmountQuarter", + "HardwareTransferAmountContinue", + "HardwareTransferSign", + "HardwareTransferOpenTrezorConnect", + "HardwareTransferSigned", + "HwTransferPassphraseSheet", + "HwTransferPassphraseInput", + "HwTransferPassphraseContinue" + ], + "sources": [ + "Bitkit/Views/Transfer/Hardware/HwPassphrasePromptSheet.swift", + "Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift", + "Bitkit/Views/Transfer/Hardware/SpendingHwSign.swift", + "Bitkit/Views/Transfer/Hardware/SpendingHwSigned.swift", + "Bitkit/Views/Wallets/HardwareWalletScreen.swift" + ], + "screens": [ + "wallet home screen", + "detail screen", + "sign screen", + "transaction signed screen", + "Processing Payment screen", + "home screen", + "passphrase sheet" + ] + }, + { + "file": "journeys/hardware-wallet/reconnect.xml", + "name": "Hardware Wallet Reconnect", + "identifiers": [ + "TrezorDisconnectButton", + "ActivityHardware", + "TrezorKnownDeviceConnect-bridge" + ], + "sources": [ + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Components/Trezor/TrezorDeviceRow.swift", + "Bitkit/Views/Trezor/TrezorConnectedView.swift" + ], + "screens": [ + "wallet home screen" + ] + }, + { + "file": "journeys/hardware-wallet/settings-hardware-wallets.xml", + "name": "Hardware Wallet Settings", + "identifiers": [ + "DrawerSettings", + "Tab-general", + "HardwareWalletsSettings", + "HardwareWalletsScreen", + "RenameHardwareWalletInput", + "RenameHardwareWalletSave", + "AddHardwareWallet", + "HardwareWalletIntroScreen", + "HardwareWalletIntroCancel", + "RemoveHwWalletDialog", + "TrezorScanButton" + ], + "sources": [ + "Bitkit/Components/DrawerView.swift", + "Bitkit/Components/RemoveHwWalletDialog.swift", + "Bitkit/Components/SegmentedControl.swift", + "Bitkit/Views/Settings/General/HardwareWalletsSettingsScreen.swift", + "Bitkit/Views/Settings/GeneralSettingsView.swift", + "Bitkit/Views/Sheets/HardwareConnectSheet.swift", + "Bitkit/Views/Sheets/RenameHardwareWalletSheet.swift", + "Bitkit/Views/Trezor/TrezorDeviceListView.swift" + ], + "screens": [ + "Hardware Wallets screen", + "Rename Hardware Wallet sheet" + ] + }, + { + "file": "journeys/hardware-wallet/suggestion-intro-sheet.xml", + "name": "Hardware Wallet Suggestion Card and Intro Sheet", + "identifiers": [ + "TrezorForgetDevice-bridge", + "ActivityHardware", + "HardwareWalletIntroScreen", + "HardwareWalletIntroContinue", + "HardwareWalletSearchingScreen", + "HardwareWalletSearchingCancel", + "HardwareWalletFoundScreen", + "HardwareWalletFoundConnect", + "HardwareWalletPairedScreen", + "HardwareWalletLabelInput", + "HardwareWalletPairedFinish" + ], + "sources": [ + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Components/Trezor/TrezorDeviceRow.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwFoundView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwPairedView.swift", + "Bitkit/Views/Sheets/HardwareConnect/HwSearchingView.swift", + "Bitkit/Views/Sheets/HardwareConnectSheet.swift" + ], + "screens": [ + "wallet home screen", + "home screen" + ] + }, + { + "file": "journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml", + "name": "Hardware Wallet Transfer To Spending MAX LSP Cap", + "identifiers": [ + "ActivityHardware", + "HardwareWalletScreen", + "HardwareTransferToSpending", + "HardwareTransferAmount", + "HardwareTransferAmountAvailable", + "HardwareTransferAmountMax", + "HardwareTransferAmountContinue", + "HardwareTransferSign", + "HardwareTransferOpenTrezorConnect", + "HardwareTransferSigned" + ], + "sources": [ + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift", + "Bitkit/Views/Transfer/Hardware/SpendingHwSign.swift", + "Bitkit/Views/Transfer/Hardware/SpendingHwSigned.swift", + "Bitkit/Views/Wallets/HardwareWalletScreen.swift" + ], + "screens": [ + "wallet home screen", + "hardware wallet detail screen", + "transfer amount screen", + "sign screen", + "transaction signed screen" + ] + }, + { + "file": "journeys/hardware-wallet/transfer-to-spending-node-warmup.xml", + "name": "Hardware Wallet Transfer To Spending Node Warmup", + "identifiers": [ + "ActivityHardware", + "HardwareTransferToSpending", + "HardwareTransferAmountQuarter", + "HardwareTransferAmountContinue", + "HardwareTransferSign" + ], + "sources": [ + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift", + "Bitkit/Views/Transfer/Hardware/SpendingHwSign.swift", + "Bitkit/Views/Wallets/HardwareWalletScreen.swift" + ], + "screens": [ + "wallet home screen", + "transfer amount screen", + "amount screen", + "sign screen" + ] + }, + { + "file": "journeys/hardware-wallet/transfer-to-spending-over-max.xml", + "name": "hardware transfer to spending amount over max is blocked", + "identifiers": [ + "ActivityHardware", + "HardwareWalletScreen", + "HardwareTransferToSpending", + "HardwareTransferAmount", + "N9", + "HardwareTransferAmountAvailable", + "HardwareTransferAmountExceededToast", + "NRemove", + "NavigationBack" + ], + "sources": [ + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Components/NavigationBar.swift", + "Bitkit/Components/NumberPad.swift", + "Bitkit/Views/Security/AuthCheck.swift", + "Bitkit/Views/Sheets/Sheet.swift", + "Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift", + "Bitkit/Views/Wallets/HardwareWalletScreen.swift" + ], + "screens": [ + "hardware wallet detail screen", + "transfer amount screen", + "hardware wallet screen" + ] + }, + { + "file": "journeys/hardware-wallet/transfer-to-spending.xml", + "name": "Hardware Wallet Transfer To Spending", + "identifiers": [ + "ActivityHardware", + "HardwareWalletScreen", + "HardwareTransferToSpending", + "HardwareTransferAmount", + "HardwareTransferAmountAvailable", + "HardwareTransferAmountQuarter", + "HardwareTransferAmountContinue", + "HardwareTransferSign", + "HardwareTransferOpenTrezorConnect", + "HardwareTransferSigned" + ], + "sources": [ + "Bitkit/Components/HardwareWalletsGrid.swift", + "Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift", + "Bitkit/Views/Transfer/Hardware/SpendingHwSign.swift", + "Bitkit/Views/Transfer/Hardware/SpendingHwSigned.swift", + "Bitkit/Views/Wallets/HardwareWalletScreen.swift" + ], + "screens": [ + "wallet home screen", + "hardware wallet detail screen", + "transfer amount screen", + "sign screen", + "transaction signed screen", + "detail screen" + ] + }, + { + "file": "journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml", + "name": "receive cjit confirm toggle requests notification permission", + "identifiers": [ + "Receive", + "ReceiveCjitAmount", + "ReceiveCjitAmountContinue", + "ReceiveCjitConfirm", + "ReceiveConfirmNotificationSwitch" + ], + "sources": [ + "Bitkit/Components/TabBar/TabBar.swift", + "Bitkit/Views/Settings/DevSettings/LegacyRnRecoveryScreen.swift", + "Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift", + "Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift" + ], + "screens": [ + "home screen", + "Receive sheet", + "amount screen", + "confirm screen" + ] + }, + { + "file": "journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml", + "name": "receive cjit liquidity toggle requests notification permission", + "identifiers": [ + "Receive", + "ReceiveCjitAmount", + "ReceiveCjitAmountContinue", + "ReceiveCjitLiquidity", + "ReceiveLiquidityNotificationSwitch" + ], + "sources": [ + "Bitkit/Components/TabBar/TabBar.swift", + "Bitkit/Views/Settings/DevSettings/LegacyRnRecoveryScreen.swift", + "Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift", + "Bitkit/Views/Wallets/Receive/ReceiveCjitLearnMore.swift" + ], + "screens": [ + "home screen", + "Receive sheet", + "amount screen", + "confirm screen", + "liquidity screen" + ] + }, + { + "file": "journeys/notification-permission/toggle-off-and-system-settings-route.xml", + "name": "notification toggle off unregisters and settings routes to system", + "identifiers": [ + "Receive", + "ReceiveCjitAmount", + "ReceiveCjitAmountContinue", + "ReceiveCjitConfirm", + "ReceiveConfirmNotificationSwitch", + "DrawerSettings", + "NotificationsOpenSystemSettings" + ], + "sources": [ + "Bitkit/Components/DrawerView.swift", + "Bitkit/Components/TabBar/TabBar.swift", + "Bitkit/Views/Settings/DevSettings/LegacyRnRecoveryScreen.swift", + "Bitkit/Views/Settings/Notifications/NotificationsSettings.swift", + "Bitkit/Views/Wallets/Receive/ReceiveCjitAmount.swift", + "Bitkit/Views/Wallets/Receive/ReceiveCjitConfirmation.swift" + ], + "screens": [ + "home screen", + "Receive sheet", + "amount screen", + "confirm screen", + "wallet home screen", + "Notifications settings screen" + ] + }, + { + "file": "journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml", + "name": "transfer spending confirm toggle requests notification permission", + "identifiers": [ + "ActivitySavings", + "TransferToSpending", + "SpendingAmount", + "SpendingAmountAvailable", + "SpendingAmountContinue", + "SpendingConfirmNotificationSwitch" + ], + "sources": [ + "Bitkit/Views/Home/HomeWalletView.swift", + "Bitkit/Views/Transfer/SpendingAmount.swift", + "Bitkit/Views/Transfer/SpendingConfirm.swift", + "Bitkit/Views/Wallets/SavingsWalletScreen.swift" + ], + "screens": [ + "home screen", + "transfer intro screen", + "spending amount screen", + "spending confirm screen" + ] + }, + { + "file": "journeys/payment-requests/issuer-interoperability.xml", + "name": "Paykit Issuer Interoperability", + "identifiers": [ + "PaymentRequestConfirm", + "PaymentRequestsBell", + "PaymentRequestsSheet", + "PaymentRequestRow-71300000-0000-4000-8000-000000000001---one-time", + "PaymentRequestPay-71300000-0000-4000-8000-000000000001", + "SendConfirmToggleDetails", + "ReviewContactRecipient" + ], + "sources": [ + "Bitkit/Components/Header.swift", + "Bitkit/Views/PaymentRequests/PaymentRequestsView.swift", + "Bitkit/Views/Wallets/Send/SendConfirmationView.swift" + ], + "screens": [ + "Payment Request confirmation screen", + "incoming Payment Requests sheet" + ] + }, + { + "file": "journeys/pubky-marketplace/wallet-leg.xml", + "name": "pubky marketplace wallet leg", + "identifiers": [ + "PubkyAuthWatchOnlyConsent", + "PubkyAuthWatchOnlyApprove", + "PubkyAuthAuthorize", + "PubkyAuthOK", + "ContactPaymentsToggle", + "Contact_", + "Contact_", + "ReviewAmount", + "PaymentRequestsBell", + "PaymentRequestsSheet", + "PaymentRequestRow----one-time", + "PaymentRequestPay-", + "ReviewContactRecipient", + "GRAB", + "SendSuccess", + "ActivityAmount", + "ActivityTxDetails" + ], + "sources": [ + "Bitkit/Components/Header.swift", + "Bitkit/Components/SwipeButton.swift", + "Bitkit/Views/Contacts/ContactsListView.swift", + "Bitkit/Views/PaymentRequests/PaymentRequestsView.swift", + "Bitkit/Views/Settings/GeneralSettingsView.swift", + "Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift", + "Bitkit/Views/Wallets/Activity/ActivityItemView.swift", + "Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift", + "Bitkit/Views/Wallets/Send/SendConfirmationView.swift", + "Bitkit/Views/Wallets/Send/SendSuccess.swift" + ], + "screens": [ + "watch-only consent screen", + "authorization screen", + "incoming requests sheet" + ] + }, + { + "file": "journeys/widgets/add-widgets-flow.xml", + "name": "add widgets flow", + "identifiers": [ + "DrawerWidgets", + "WidgetsAdd", + "WidgetListItem-calculator", + "WidgetSave" + ], + "sources": [ + "Bitkit/Components/DrawerView.swift", + "Bitkit/Views/Home/HomeWidgetsView.swift", + "Bitkit/Views/Widgets/WidgetPreviewSheetView.swift", + "Bitkit/Views/Widgets/WidgetsListSheetView.swift" + ], + "screens": [ + "Add Widget sheet" + ] + }, + { + "file": "journeys/widgets/widgets-intro.xml", + "name": "widgets intro", + "identifiers": [ + "DrawerWidgets", + "WidgetsOnboarding", + "WidgetsOnboardingAddWidget", + "WidgetListItem-weather", + "WidgetSave" + ], + "sources": [ + "Bitkit/Components/DrawerView.swift", + "Bitkit/Views/Widgets/WidgetPreviewSheetView.swift", + "Bitkit/Views/Widgets/WidgetsIntroView.swift", + "Bitkit/Views/Widgets/WidgetsListSheetView.swift" + ], + "screens": [ + "widgets intro screen", + "Add Widget sheet" + ] + } +] diff --git a/scripts/journeys_index.py b/scripts/journeys_index.py new file mode 100755 index 000000000..50dbdfc08 --- /dev/null +++ b/scripts/journeys_index.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +r"""Generate journeys/index.json: the identifiers, source files and screens each journey names. + +For each journeys/**/*.xml file the index records the file, the , the identifiers its +actions name, the source files that declare those identifiers, and the screens its actions name. + +A source file declares an identifier when it contains it as a string literal, or as a string template +whose text before the first interpolation begins the identifier while the rest of the identifier has +no capital letters: "N$text" and "N\(number)" declare N9 but not NRemove. + + python3 scripts/journeys_index.py write journeys/index.json + python3 scripts/journeys_index.py --check fail when journeys/index.json is stale + +Both modes fail when a journey names an identifier that no source file declares. +""" + +import argparse +import json +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +INDEX = ROOT / "journeys" / "index.json" + +# Platform: an action names an identifier as id "Name"; Swift sources declare it. +IDENTIFIER_MENTION = re.compile(r'\bid "([^"]+)"') +SOURCE_DIRS = ("Bitkit", "BitkitNotification", "BitkitWidget") +SOURCE_SUFFIX = ".swift" + +# A string literal "Name", or a string template "Name$…" or "Name\(…)" that begins with Name. +STRING_START = re.compile(r'"([\w-]+)("|\$|\\\()') +SCREEN_WORD = r'(?:"[^"]+"|(?!(?:a|an|bottom|its|same|the)\b)[\w-]+)' +SCREEN_MENTION = re.compile( + rf"\b(?:[Tt]he|[Aa]n?|[Ii]ts)\s+({SCREEN_WORD}(?:\s+{SCREEN_WORD}){{0,3}}?)\s+(screen|(?:bottom\s+)?sheet)\b" +) + + +def scan_sources(): + literals, prefixes = {}, {} + for source_dir in SOURCE_DIRS: + for path in (ROOT / source_dir).rglob("*" + SOURCE_SUFFIX): + relative = path.relative_to(ROOT).as_posix() + for text, end in STRING_START.findall(path.read_text(encoding="utf-8", errors="replace")): + declarations = literals if end == '"' else prefixes + declarations.setdefault(text, set()).add(relative) + return literals, prefixes + + +def declaring_files(identifier, literals, prefixes): + if identifier in literals: + return literals[identifier] + for end in range(len(identifier) - 1, 0, -1): + prefix, rest = identifier[:end], identifier[end:] + if prefix in prefixes: + return set() if re.search("[A-Z]", rest) else prefixes[prefix] + return set() + + +def build_index(): + literals, prefixes = scan_sources() + journeys, errors = [], [] + for path in sorted((ROOT / "journeys").rglob("*.xml")): + relative = path.relative_to(ROOT).as_posix() + try: + root = ET.parse(path).getroot() + except ET.ParseError as error: + errors.append(f"{relative}: {error}") + continue + actions = [" ".join("".join(action.itertext()).split()) for action in root.iter("action")] + identifiers = list(dict.fromkeys(name for action in actions for name in IDENTIFIER_MENTION.findall(action))) + sources = set() + for identifier in identifiers: + files = declaring_files(identifier, literals, prefixes) + if not files: + errors.append(f'{relative}: identifier "{identifier}" is not declared in any source file') + sources |= files + screens = [ + " ".join(name.replace('"', "").split() + [kind]) + for action in actions + for name, kind in SCREEN_MENTION.findall(action) + ] + journeys.append( + { + "file": relative, + "name": root.get("name", ""), + "identifiers": identifiers, + "sources": sorted(sources), + "screens": list(dict.fromkeys(screens)), + } + ) + return journeys, errors + + +def main(): + parser = argparse.ArgumentParser(description="Generate or check journeys/index.json.") + parser.add_argument("--check", action="store_true", help="fail when journeys/index.json is stale") + check = parser.parse_args().check + + journeys, errors = build_index() + generated = json.dumps(journeys, indent=2, ensure_ascii=False) + "\n" + if not check: + INDEX.write_text(generated, encoding="utf-8") + elif not INDEX.exists() or INDEX.read_text(encoding="utf-8") != generated: + errors.insert(0, "journeys/index.json is stale; run `python3 scripts/journeys_index.py` and commit it") + + for error in errors: + print(error, file=sys.stderr) + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main())