diff --git a/.agents/commands/pr.md b/.agents/commands/pr.md index 18f25b53fa..d63c8a38a2 100644 --- a/.agents/commands/pr.md +++ b/.agents/commands/pr.md @@ -38,6 +38,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-android`) @@ -45,6 +50,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$'` - Read `docs/screens-map.md` as the starting point for locating relevant Figma frames when the diff has user-visible UI changes - **If custom instructions provided:** - If instructions reference a specific commit SHA (pattern like `commit [a-f0-9]{7,40}`): @@ -133,9 +139,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, Gradle tasks, detekt, lint, 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, each with the test file name and a short explanation. - Reference test files by bare file name only (e.g. `HwWalletRepoTest.kt`), never the full path. Only when two referenced test files share the same name, prefix the shortest leading path segment(s) that disambiguate them (e.g. `repositories/FooTest.kt` vs `viewmodels/FooTest.kt`). @@ -147,7 +157,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.**`. @@ -184,16 +194,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 `SendInvoiceTest.kt`. - Unit tests modified: update channel navigation assertions in `ChannelDetailTest.kt`. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a8c3a59701..b148fd336b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,5 +19,14 @@ ### QA Notes - +#### Journeys + + + +#### Manual Tests + + + +#### Automated Checks + diff --git a/.github/workflows/journeys.yml b/.github/workflows/journeys.yml new file mode 100644 index 0000000000..7a4992f2b2 --- /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 1a5913c006..c4092e311b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -262,6 +262,15 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - ALWAYS use `.toImmutableList()`, `.toImmutableMap()`, `.toImmutableSet()` when producing collections for UI state - ALWAYS use `persistentListOf()`, `persistentMapOf()`, `persistentSetOf()` for default values in UiState fields +### Journeys + +`journeys/` holds XML walkthroughs of app behaviour that an agent evaluates on a running emulator. They are the QA contract for PRs. + +- ALWAYS 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 +- ALWAYS name an identifier in a journey action as `testTag "Name"` +- ALWAYS 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 +- ALWAYS list the journeys a PR adds or updates under `#### Journeys` in its QA Notes + ### Changelog - NEVER edit `CHANGELOG.md` in normal feature/fix PRs; release automation collects changelog fragments into it diff --git a/journeys/index.json b/journeys/index.json new file mode 100644 index 0000000000..5cc9290445 --- /dev/null +++ b/journeys/index.json @@ -0,0 +1,670 @@ +[ + { + "file": "journeys/amount-limits/external-amount-over-max.xml", + "name": "external node amount over max is blocked", + "identifiers": [ + "FundManual", + "NodeIdInput", + "HostInput", + "PortInput", + "ExternalContinue", + "ExternalAmount", + "N9", + "ExternalAmountNumberField", + "NRemove" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/components/NumberPad.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/FundingAdvancedScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalAmountScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt", + "app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt" + ], + "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", + "send_amount_screen", + "available_balance", + "N9", + "SendNumberField", + "NRemove" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/components/NumberPad.kt", + "app/src/main/java/to/bitkit/ui/components/TabBar.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt" + ], + "screens": [ + "Send amount screen" + ] + }, + { + "file": "journeys/amount-limits/transfer-spending-advanced-over-max.xml", + "name": "receiving capacity amount over max is blocked", + "identifiers": [ + "SpendingAmount", + "SpendingAmountAvailable", + "SpendingAmountQuarter", + "SpendingAmountContinue", + "SpendingAdvanced", + "N9", + "SpendingAdvancedNumberField", + "NRemove" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/components/NumberPad.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt" + ], + "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": [ + "SpendingAmount", + "SpendingAmountAvailable", + "N9", + "SpendingAmountNumberField", + "NRemove" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/components/NumberPad.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt" + ], + "screens": [ + "home screen", + "transfer intro screen", + "spending amount screen" + ] + }, + { + "file": "journeys/cjit-notifications/cjit-foreground-service-notification.xml", + "name": "cjit notification via foreground service is single and formatted", + "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": [ + "app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt" + ], + "screens": [ + "wallet home screen" + ] + }, + { + "file": "journeys/deeplinks/screen-deeplink.xml", + "name": "screen deeplink", + "identifiers": [], + "sources": [], + "screens": [ + "Settings screen", + "log detail screen" + ] + }, + { + "file": "journeys/deeplinks/sheet-deeplink.xml", + "name": "sheet deeplink", + "identifiers": [], + "sources": [], + "screens": [ + "Send sheet" + ] + }, + { + "file": "journeys/hardware-wallet/activity-blue-icons.xml", + "name": "Hardware Wallet Activity Blue Icons", + "identifiers": [], + "sources": [], + "screens": [ + "wallet home screen", + "detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/activity-detail-hw-tags.xml", + "name": "Hardware Wallet Activity Tags And Inputs Outputs", + "identifiers": [], + "sources": [], + "screens": [ + "wallet home screen", + "activity detail screen", + "Activity Explorer screen" + ] + }, + { + "file": "journeys/hardware-wallet/connect-flow.xml", + "name": "Hardware Wallet Connect Flow From Settings", + "identifiers": [ + "HardwareWalletsScreen", + "AddHardwareWallet", + "HardwareWalletIntroScreen", + "HardwareWalletIntroContinue", + "HardwareWalletSearchingScreen", + "HardwareWalletFoundScreen", + "HardwareWalletFoundConnect", + "HardwareWalletPairedScreen", + "HardwareWalletLabelInput", + "HardwareWalletPairedFinish" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwFoundSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwPairedSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwSearchingSheet.kt" + ], + "screens": [ + "Hardware Wallets screen" + ] + }, + { + "file": "journeys/hardware-wallet/connect-home-tile.xml", + "name": "Hardware Wallet Connect and Home Tile", + "identifiers": [ + "HardwareWalletScreen" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt" + ], + "screens": [ + "wallet home screen", + "hardware wallet detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/detail-overview.xml", + "name": "Hardware Wallet Detail Overview", + "identifiers": [ + "HardwareWalletScreen", + "HardwareTransferToSpending", + "HardwareTransferAmount", + "RemoveHardwareWallet" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingAmountHwScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt" + ], + "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": [ + "AddHardwareWallet", + "HardwareWalletIntroContinue", + "HardwareWalletFoundConnect", + "HardwareWalletPairedPassphrase", + "HardwareWalletPassphraseInput", + "HardwareWalletPassphraseContinue", + "HardwareWalletPassphraseScreen" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwFoundSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwPairedSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwPassphraseSheet.kt" + ], + "screens": [ + "wallet home screen" + ] + }, + { + "file": "journeys/hardware-wallet/passphrase-pairing.xml", + "name": "Hardware Wallet Passphrase Pairing", + "identifiers": [ + "AddHardwareWallet", + "HardwareWalletIntroContinue", + "HardwareWalletFoundScreen", + "HardwareWalletFoundConnect", + "HardwareWalletPairedScreen", + "HardwareWalletPairedPassphrase", + "HardwareWalletPairedFinish", + "HardwareWalletPassphraseScreen", + "HardwareWalletPassphraseContinue", + "HardwareWalletPassphraseInput", + "HardwareWalletPassphrasePairedScreen", + "HardwareWalletLabelInput", + "HardwareWalletScreen" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt", + "app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwFoundSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwPairedSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwPassphrasePairedSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwPassphraseSheet.kt" + ], + "screens": [ + "wallet home screen", + "hardware wallet detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/passphrase-settings-remove.xml", + "name": "Hardware Wallet Passphrase Settings And Remove", + "identifiers": [ + "HardwareWalletsScreen", + "RenameHardwareWalletInput", + "RenameHardwareWalletSave" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt" + ], + "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": [ + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/HwPassphrasePromptSheet.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingAmountHwScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingHwSignScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingHwSignedScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt" + ], + "screens": [ + "wallet home screen", + "detail screen", + "sign screen", + "transaction signed screen", + "Processing Payment screen", + "home screen", + "passphrase sheet" + ] + }, + { + "file": "journeys/hardware-wallet/receive-onchain.xml", + "name": "Hardware Wallet On-chain Receive", + "identifiers": [ + "Receive", + "ReceiveScreen", + "QRCode", + "ShowDetails", + "ReceiveHardwareAddress", + "HardwareVerifyAddress" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/components/TabBar.kt", + "app/src/main/java/to/bitkit/ui/screens/settings/LegacyRnRecoveryScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt" + ], + "screens": [ + "wallet home screen", + "Receive sheet" + ] + }, + { + "file": "journeys/hardware-wallet/send-onchain.xml", + "name": "Hardware Wallet On-chain Send", + "identifiers": [ + "Send", + "RecipientManual", + "RecipientInput", + "AddressContinue", + "send_amount_screen", + "AssetButton-switch", + "ContinueAmount", + "SendConfirmAssetButton", + "HardwareSendAmount", + "HardwareSendAddress", + "HardwareSendOpenTrezorConnect", + "SendSuccess" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/components/TabBar.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/HwSendSignScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt", + "app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt" + ], + "screens": [ + "wallet home screen", + "Send amount screen", + "Send review screen", + "hardware sign screen", + "success screen" + ] + }, + { + "file": "journeys/hardware-wallet/settings-hardware-wallets.xml", + "name": "Hardware Wallet Settings", + "identifiers": [ + "HardwareWalletsScreen", + "RenameHardwareWalletInput", + "RenameHardwareWalletSave", + "AddHardwareWallet", + "HardwareWalletIntroScreen" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/settings/general/HardwareWalletsSettingsScreen.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt" + ], + "screens": [ + "Hardware Wallets screen", + "Rename Hardware Wallet bottom sheet" + ] + }, + { + "file": "journeys/hardware-wallet/suggestion-intro-sheet.xml", + "name": "Hardware Wallet Suggestion Card and Intro Sheet", + "identifiers": [ + "HardwareWalletIntroScreen", + "HardwareWalletIntroContinue", + "HardwareWalletSearchingScreen", + "HardwareWalletSearchingCancel", + "HardwareWalletFoundScreen", + "HardwareWalletFoundConnect", + "HardwareWalletPairedScreen", + "HardwareWalletLabelInput", + "HardwareWalletPairedFinish" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwFoundSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwIntroSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwPairedSheet.kt", + "app/src/main/java/to/bitkit/ui/sheets/hardware/HwSearchingSheet.kt" + ], + "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": [ + "HardwareWalletScreen", + "HardwareTransferToSpending", + "HardwareTransferAmount", + "HardwareTransferAmountMax", + "HardwareTransferAmountContinue", + "HardwareTransferSign", + "HardwareTransferOpenTrezorConnect", + "HardwareTransferSigned" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingAmountHwScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingHwSignScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingHwSignedScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt" + ], + "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": [ + "HardwareTransferToSpending", + "HardwareTransferAmountQuarter", + "HardwareTransferAmountContinue", + "HardwareTransferSign" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingAmountHwScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingHwSignScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt" + ], + "screens": [ + "wallet home screen", + "transfer amount screen", + "amount screen", + "sign screen" + ] + }, + { + "file": "journeys/hardware-wallet/transfer-to-spending.xml", + "name": "Hardware Wallet Transfer To Spending", + "identifiers": [ + "HardwareWalletScreen", + "HardwareTransferToSpending", + "HardwareTransferAmount", + "HardwareTransferAmountQuarter", + "HardwareTransferAmountContinue", + "HardwareTransferSign", + "HardwareTransferOpenTrezorConnect", + "HardwareTransferSigned" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingAmountHwScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingHwSignScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/hardware/SpendingHwSignedScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/HardwareWalletScreen.kt" + ], + "screens": [ + "wallet home screen", + "hardware wallet detail screen", + "transfer amount screen", + "sign screen", + "transaction signed screen", + "detail screen" + ] + }, + { + "file": "journeys/hardware-wallet/usb-reconnect.xml", + "name": "Hardware Wallet USB Reconnect", + "identifiers": [], + "sources": [], + "screens": [ + "wallet home screen" + ] + }, + { + "file": "journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml", + "name": "receive cjit confirm toggle requests notification permission", + "identifiers": [ + "ReceiveConfirmNotificationSwitch" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt" + ], + "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": [ + "ReceiveLiquidityNotificationSwitch" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt" + ], + "screens": [ + "home screen", + "Receive sheet", + "amount screen", + "confirm screen", + "liquidity screen" + ] + }, + { + "file": "journeys/notification-permission/toggle-off-opens-system-settings.xml", + "name": "notification toggle off opens system settings", + "identifiers": [ + "ReceiveConfirmNotificationSwitch" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt" + ], + "screens": [ + "home screen", + "Receive sheet", + "amount screen", + "confirm screen", + "Android system notification settings screen" + ] + }, + { + "file": "journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml", + "name": "transfer spending confirm toggle requests notification permission", + "identifiers": [ + "SpendingAmount", + "SpendingAmountAvailable", + "SpendingAmountContinue", + "SpendingConfirmNotificationSwitch" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt" + ], + "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", + "PaymentRequestPay-71300000-0000-4000-8000-000000000001", + "SendConfirmToggleDetails", + "ReviewContactRecipient" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt" + ], + "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-", + "PaymentRequestPay-", + "ReviewContactRecipient", + "GRAB", + "SendSuccess", + "ActivityAmount", + "ActivityTxDetails" + ], + "sources": [ + "app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt", + "app/src/main/java/to/bitkit/ui/screens/contacts/ContactsScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt", + "app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt", + "app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt", + "app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt" + ], + "screens": [ + "watch-only consent screen", + "authorization screen", + "incoming requests sheet" + ] + }, + { + "file": "journeys/widgets/add-widgets-flow.xml", + "name": "add widgets flow", + "identifiers": [], + "sources": [], + "screens": [ + "Add Widget bottom sheet" + ] + }, + { + "file": "journeys/widgets/widgets-intro.xml", + "name": "widgets intro", + "identifiers": [], + "sources": [], + "screens": [ + "Add Widget bottom sheet" + ] + } +] diff --git a/scripts/journeys_index.py b/scripts/journeys_index.py new file mode 100755 index 0000000000..d94d5112f3 --- /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 testTag "Name"; Kotlin sources declare it. +IDENTIFIER_MENTION = re.compile(r'\btestTag "([^"]+)"') +SOURCE_DIRS = ("app/src/main", "app/src/debug", "app/src/release") +SOURCE_SUFFIX = ".kt" + +# 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())