diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreenTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreenTest.kt index 35fd856b3d..18fdf8a079 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreenTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreenTest.kt @@ -47,7 +47,8 @@ class PaymentRequestsScreenTest { } } - composeTestRule.onNodeWithTag("PaymentRequestRowincoming").assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestRow-incoming").assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestPay-incoming").assertIsDisplayed() composeTestRule.onNodeWithTag("MoneyPrimary").assertIsDisplayed() composeTestRule.onNodeWithTag("MoneySecondary").assertIsDisplayed() composeTestRule.onNodeWithTag("PaymentRequestsSeeAll").assertIsDisplayed() @@ -105,8 +106,8 @@ class PaymentRequestsScreenTest { } } - composeTestRule.onNodeWithTag("PaymentRequestRowaccepted").assertIsDisplayed() - composeTestRule.onNodeWithTag("PaymentRequestRowoutgoing").assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestRow-accepted").assertIsDisplayed() + composeTestRule.onNodeWithTag("PaymentRequestRow-outgoing").assertIsDisplayed() composeTestRule.onNodeWithText("Waiting for", substring = true).assertIsDisplayed() composeTestRule.onNodeWithText("PAYMENT REQUESTS").assertIsDisplayed() composeTestRule.onNodeWithText("TODAY").assertIsDisplayed() diff --git a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt index abdab879f5..9ff28c241e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/paymentrequests/PaymentRequestsScreen.kt @@ -490,7 +490,7 @@ internal fun PaymentRequestCard( Modifier } ) - .testTag("PaymentRequestRow${request.paymentRequestId}"), + .testTag("PaymentRequestRow-${request.paymentRequestId}") ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -553,7 +553,9 @@ internal fun PaymentRequestCard( ) }, size = ButtonSize.Small, - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .testTag("PaymentRequestPay-${request.paymentRequestId}") ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt index 8cb85ac8a7..b63d393506 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyAuthApprovalSheet.kt @@ -350,7 +350,9 @@ private fun ColumnScope.AuthorizeContent( PrimaryButton( text = stringResource(R.string.profile__auth_approval_authorize), onClick = onAuthorize, - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .testTag("PubkyAuthAuthorize") ) } VerticalSpacer(16.dp) @@ -429,6 +431,7 @@ private fun ColumnScope.SuccessContent( PrimaryButton( text = stringResource(R.string.profile__auth_approval_ok), onClick = onDismiss, + modifier = Modifier.testTag("PubkyAuthOK") ) VerticalSpacer(16.dp) } diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index d8e88edde8..c037f21dc8 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -343,7 +343,7 @@ private fun GeneralTabContent( icon = { SettingsIcon(R.drawable.ic_coins) }, onClick = { onEvent(SettingsEvent.ContactPaymentsClick) }, enabled = !state.isUpdatingContactPayments, - switchTestTag = "ContactPaymentsSwitch", + switchTestTag = "ContactPaymentsToggle", modifier = Modifier.testTag("ContactPaymentsSettings") ) } diff --git a/journeys/pubky-marketplace/README.md b/journeys/pubky-marketplace/README.md new file mode 100644 index 0000000000..ff80d54c7e --- /dev/null +++ b/journeys/pubky-marketplace/README.md @@ -0,0 +1,159 @@ +# Pubky marketplace wallet leg + +This suite covers the two-wallet Bitkit leg of a Pubky marketplace purchase: a seller grants a +watch-only account claim, a linked buyer receives the resulting Payment Request, and the buyer pays +the request on regtest through confirmation. It does not cover marketplace browsing, Locks content +delivery, fiat payment, or Hypercolor. + +## Required integration fixture runtime + +The journey needs a controlled integration fixture runtime. It must provide: + +- A fresh Pubky testnet or isolated staging namespace reachable by both wallets. +- A Paykit Server including the canonical request behavior from merged upstream + [`pubky/paykit-server#2`](https://github.com/pubky/paykit-server/pull/2), plus a `/setup` flow whose + auth URL carries `x-bitkit-claim=watch-only-account-v1`. +- A regtest bitcoind and Electrum/Fulcrum endpoint on the same chain. Configure the endpoint in both + wallets before their first launch so neither wallet retains a taller foreign regtest tip. +- A clean seller wallet, a separate clean funded buyer wallet, and the seller Pubky public key. +- A marketplace driver that can create one purchase for the buyer, expose its Payment Request id, + report Paykit delivery, return the derived on-chain address and expected amount, mine one block, + and report the transaction and purchase status. + +Android emulators reach host services through `10.0.2.2`. When the Pubky testnet runtime advertises +its homeserver endpoints as localhost, map its TCP services into each emulator before creating a +profile: + +```sh +adb -s reverse tcp:6286 tcp:6286 +adb -s reverse tcp:6287 tcp:6287 +adb -s reverse tcp:6288 tcp:6288 +adb -s reverse tcp:15411 tcp:15411 +adb -s reverse tcp:15412 tcp:15412 +``` + +After creating each wallet, enable Paykit UI in Dev Settings (`PaykitUiToggle`), confirm the warning, +and verify activation (`PaykitUiEnabledToast`). Then choose **Create profile with Bitkit** to create +a Bitkit-generated Pubky identity in each wallet. Do not import the identity with Pubky Ring; an +imported identity cannot approve the fixture setup auth URL. + +The request and endpoint must satisfy the issuer contract from Android issue +[#1208](https://github.com/synonymdev/bitkit-android/issues/1208): lowercase `btc`, a +network-correct `btc-regtest-*` endpoint identifier, and a JSON endpoint payload with a non-empty +string `value`. The fixture must keep watch-only account material and spending authority separate. +Evidence must show the claimed account xpub and account index while omitting wallet seed material +and tokens. + +Use the pinned +[`BitcoinErrorLog/pubky-marketplace/payments-env`](https://github.com/BitcoinErrorLog/pubky-marketplace/tree/ed03a32ecfe02deab40ad10ae1bac7fa18465c10/payments-env) +runtime as the marketplace driver and Locks harness. Fixture commit `ed03a32e` pins Paykit Server +source `867fc883` and verifies the canonical lowercase asset, network-correct endpoint identifier, +JSON value payload, and initial private-link retry behavior. Its `scripts/verify.sh` proves the +Locks, Paykit, Pubky, bitcoind, and Fulcrum protocol path. The seller wallet fills the +companion-auth role and the buyer wallet fills the reader role; the other fixture roles remain +unchanged. + +## Required app changes + +The full journey depends on the sibling work from the parent epic: + +- [#1208](https://github.com/synonymdev/bitkit-android/issues/1208) defines the issuer interop + contract. +- [#1209](https://github.com/synonymdev/bitkit-android/issues/1209) adds reason-specific parse + diagnostics and terminal feedback when an open-time Pay retry is exhausted. +- [#1210](https://github.com/synonymdev/bitkit-android/issues/1210) owns the approved Payment Request + intake policy. This journey does not implement or widen that policy. +- [#1211](https://github.com/synonymdev/bitkit-android/issues/1211) prevents an Electrum-rejected + broadcast from reaching `SendSuccess`. +- [#1218](https://github.com/synonymdev/bitkit-android/issues/1218) tracks the incoming on-chain + request swipe requirement. The behavior is supplied by merged + [#1178](https://github.com/synonymdev/bitkit-android/pull/1178) at `9698dea4`. + +The linked-contact prerequisite is existing Paykit behavior: the buyer must save the seller before +Bitkit's private-message poll can receive the request. The seller must also save the buyer when the +fixture exercises bilateral private delivery. + +## Evidence contract + +Capture one timestamped evidence directory per run. Record the app commit, fixture runtime +revisions, both device identifiers, Payment Request id, transaction id, and regtest block height. +Keep these artifacts at each boundary: + +| Boundary | Bitkit evidence | Fixture evidence | +| --- | --- | --- | +| Watch-only claim | `PubkyAuthWatchOnlyConsent`, `PubkyAuthWatchOnlyApprove`, `PubkyAuthAuthorize`, and `PubkyAuthOK` snapshots | Setup completion and the claimed xpub/account index, with no spending key | +| Contact payments | `ContactPaymentsToggle` snapshots from both wallets | Public receiver markers for both wallet identities | +| Linked buyer | `Contact_` snapshot | Seller and buyer peer-link state | +| Incoming request | `PaymentRequestsSheet` and `PaymentRequestRow-` snapshots showing seller, amount, and note when present | Delivery record and exact Payment Request id | +| Payment approval | `PaymentRequestPay-`, `ReviewAmount`, and `ReviewContactRecipient` snapshots | Derived regtest address and expected amount | +| Broadcast | `SendSuccess` snapshot and buyer activity details | Transaction in the fixture mempool with an amount-matched output | +| Confirmation | Confirmed buyer activity snapshot | Transaction id at one or more confirmations and completed purchase status | + +`SendSuccess` is evidence of backend acceptance, not confirmation. The fixture's chain and purchase +status are the confirmation authority. `PaymentRequestPay-` is shared with the +iOS counterpart supplied by [`bitkit-ios#721`](https://github.com/synonymdev/bitkit-ios/pull/721). + +## Release provenance + +The watch-only claim entered the repository in `6f3134e1`, and the incoming Payment Request surface +entered in `4ea3dd16` and `7385376d`. No shipped Android release or tag contains both surfaces as of +2026-09-02. The first intended shipped release is the open `2.6.0` milestone; record its final tag +here when it ships. + +## Acceptance run from 2026-09-02 + +The initial successful payment replay used an isolated runtime including upstream Paykit Server +merge `867fc883` and a pre-merge copy of the incoming-request swipe behavior now supplied by merged +[#1178](https://github.com/synonymdev/bitkit-android/pull/1178). The installed E2E APK had SHA-256 +`cb494d870a255b96e3e289cbe7e659aa89fff1987308502b2fd55f121a0b0c42`, used the local backend at +`10.0.2.2`, and targeted homeserver +`8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo`. A deployed seller completed the unchanged +watch-only consent, approval, authorization, companion-claim delivery, and `/setup` completion +path. The fixture verifier passed with lowercase `btc`, endpoint identifier +`btc-regtest-p2wpkh`, and a JSON string `value`. + +Fresh Android buyer `pubkycecq8ssqnfgfwifioj7djoutnupmpjgomobistnz34zd9d5yyn4o` linked seller +`pubkyhbn4tahj71yzpmtarz5amtqqf5fmicdd7rs8ao448tzaujdapfiy`; both wallets saved the reciprocal +contact and enabled contact payments. The buyer received 1,000,000 sats at +`bcrt1q6mkkp26tu8zm4g78d58uksmvv3una04dryp7s0` in transaction +`3b48b259cd9aec8817b902a44c1609738268880cb16f25179ab87dea21a81a11`, confirmed at height +16,397 in block `5ad00a6d1d9e0e5a2348a255eae5bc83d507a759a4e9f377567af92fa3bacef6`. + +The fixture delivered Locks bundle `1GC8SBDYB2HHA2E0NZ51ZVEZX4`, server invoice +`92fdee57-6a46-40f2-9714-8a0e68d7e60e`, Payment Request +`ad1a8463-59e0-4cf8-b037-99ffb9d5b6ca`, and event +`4282bad8-270d-4e5c-a5f9-bca52d1583dd`. Android displayed the incoming Seller row for 15,000 +sats with an absent note, opened the payment review, resolved +`bcrt1qkuajc36azmaf9kk9ndwy9rdttl6vdlqwtvgyg5`, preserved the amount and Seller recipient, and +enabled the confirmation slider with a 141-sat fee. + +One swipe broadcast transaction `a3e2801d8cf3afa6a461a30fa38bc46680602311a7728b97e5d88f3767f3d9d2`. +The frozen zero-confirmation boundary contained only that mempool transaction, whose output zero +paid exactly 15,000 sats to the derived address; Paykit reported +`detected/0/amount_matched=true` and Locks remained pending. The fixture then mined exactly one +block. Android synced height 16,398 and showed a confirmed Seller activity with a 15,000-sat +payment and 141-sat fee. The transaction confirmed in block +`5f2a356cace276a17e0759d8d34e7c196c852b4b299901e592552fb8f8f0bb19`, Paykit reported +`confirmed/1/amount_matched=true`, the Locks bundle completed, and the paid request remained as +history without Pay or Dismiss actions. + +The selector-specific acceptance replay used Android buyer `emulator-5560` and seller iOS simulator +`B379B7A4-715A-427F-8CB6-A6479BC73050`. It ran a pre-merge integration build containing the journey +selectors and the incoming-request swipe behavior now supplied by merged #1178. The built and +device-installed APKs both had SHA-256 +`8d62881da626a6f6eab1e243c05079305ebd3efd2f9abb820a1a6b55d6454cf4`. The retained Buyer profile +was synced at height 16,398 with 984,859 sats before the fixture created Locks bundle +`J9AKW3TNASDM6MJB0SNE08RJ0M`, server invoice `8d810705-6eeb-46f6-9c57-dd3bc4bdc0cf`, Payment +Request `b7f67854-b052-4eed-a6e7-e1ac41c31a7a`, event +`cec538cb-57f5-4c89-9d80-7584699af1ec`, and payment reference +`94f212c9-ed8c-4b85-9b0a-e9eea09f0a73`. + +Android exposed the exact `PaymentRequestRow-b7f67854-b052-4eed-a6e7-e1ac41c31a7a` and +`PaymentRequestPay-b7f67854-b052-4eed-a6e7-e1ac41c31a7a` selectors, then preserved the 15,000-sat +amount, Seller recipient, and 141-sat fee with an enabled confirmation slider. One swipe broadcast +transaction `3dacd7591e0e9d1b7eab62f814f86017c41c1a1b8dda839a79b7244d9b20f997`, whose output zero paid +exactly 15,000 sats to `bcrt1qxluk70usejytg7ehgdhpsq657eshhmr8lmkcsv`. The frozen +zero-confirmation boundary contained that single mempool transaction. The fixture mined exactly one +block, `7cabfa65673a0472d70c0b1f217e93d6114e040a342381a446f9a50987d18f30`, at height 16,399. Paykit +reported `confirmed/1/amount_matched=true`, the Locks bundle completed, Android showed the Seller +payment as confirmed, and the paid request remained in history without Pay or Dismiss actions. diff --git a/journeys/pubky-marketplace/wallet-leg.xml b/journeys/pubky-marketplace/wallet-leg.xml new file mode 100644 index 0000000000..891e0e0ce8 --- /dev/null +++ b/journeys/pubky-marketplace/wallet-leg.xml @@ -0,0 +1,46 @@ + + + Verifies a Bitkit seller grants a watch-only account claim and a separate linked Bitkit buyer + receives, approves, pays, and confirms the resulting marketplace Payment Request on regtest. + Precondition: two clean wallets and the integration fixture runtime described in this suite's + README. + Configure the fixture Electrum endpoint and Android emulator port mappings before either + wallet's first launch. After creating both wallets, enable Paykit UI in Dev Settings (testTag + "PaykitUiToggle"), confirm the warning, and verify activation (testTag "PaykitUiEnabledToast"). + Then create a Bitkit-generated Pubky identity in each wallet. Pubky Ring-imported identities + cannot approve the fixture setup auth URL. Start with the seller wallet open and the fixture's + fresh setup auth URL available. + + + Open the fixture setup auth URL in the seller wallet + Verify the watch-only consent screen (testTag "PubkyAuthWatchOnlyConsent") is visible + Capture the watch-only consent evidence, then tap Approve (testTag "PubkyAuthWatchOnlyApprove") + Verify the authorization screen shows exactly /pub/paykit/v0/bitkit/server and /pub/paykit/v0/private/bitkit/server, each with READ, WRITE access + Tap Authorize (testTag "PubkyAuthAuthorize") + Verify authorization succeeds (testTag "PubkyAuthOK") + Verify the fixture completes setup with the seller account xpub and no spending authority + Open General Settings in both wallets and verify contact payments (testTag "ContactPaymentsToggle") are enabled + Open Contacts in the buyer wallet and save the fixture's seller public key + Verify the seller contact (testTag "Contact_<seller-public-key>") is visible + Open Contacts in the seller wallet and save the buyer public key + Verify the buyer contact (testTag "Contact_<buyer-public-key>") is visible + Verify the fixture reports the seller and buyer as linked peers + Return the buyer wallet to Home + Have the fixture create one marketplace purchase for the buyer and record its Payment Request id + Verify the request uses lowercase "btc", a regtest endpoint identifier, and a JSON endpoint value + Verify the fresh request opens payment review automatically (testTag "ReviewAmount") + Press Android back to dismiss the automatic review, verify the requests bell (testTag "PaymentRequestsBell") is visible, then tap it + Verify the incoming requests sheet (testTag "PaymentRequestsSheet") is visible + Verify the request row (testTag "PaymentRequestRow-<payment-request-id>") shows the seller and expected amount, plus the fixture note when present + Capture the incoming-request evidence, then tap Pay (testTag "PaymentRequestPay-<payment-request-id>") + Verify the review amount (testTag "ReviewAmount") matches the fixture amount + Show details and verify the recipient (testTag "ReviewContactRecipient") is the seller contact + Capture the payment-approval evidence, then swipe to send (testTag "GRAB") + Verify Bitcoin Sent (testTag "SendSuccess") appears after the backend accepts the transaction + Verify the fixture mempool contains the transaction with an amount-matched output + Mine one block with the fixture and wait for both Bitkit and the marketplace status to refresh + Open the latest sent on-chain activity and verify its amount (testTag "ActivityAmount") and transaction details (testTag "ActivityTxDetails") + Verify the fixture reports the transaction confirmed and the marketplace purchase completed + Capture the final activity, transaction id, confirmation height, and completed purchase evidence + +