Skip to content

fix: pay first onchain subscription period in one swipe - #1422

Merged
jvsena42 merged 18 commits into
masterfrom
fix/subscription-onchain-single-swipe
Oct 13, 2026
Merged

jvsena42 merged 18 commits into
masterfrom
fix/subscription-onchain-single-swipe

Conversation

@jvsena42

@jvsena42 jvsena42 commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Closes #1468

Stacked on #1421.

This PR makes accepting a subscription a single swipe when the first payment is paid on-chain, matching the Subscriptions flow in Figma and the existing Lightning behaviour.

Description

  • Before, an on-chain first payment took two swipes: Swipe To Subscribe & Pay on the Review sheet only subscribed, then the send confirmation opened with a second Swipe To Subscribe & Pay to pay. Figma shows one swipe from Review & Subscribe to Subscribed, and its flow notes do not limit that to Lightning.
  • SendUiState.shouldAutomaticallyPay no longer requires the Lightning pay method, so the first payment due on acceptance starts by itself for on-chain savings too.
  • While that first payment is sent, the sheet keeps the Review & Subscribe layout (amount, subscription card, illustration) with the swipe in its loading state, for Lightning and on-chain. Before, the send step replaced it with a bare progress indicator.
  • That kept layout is inert: the subscription card is not tappable, and the swipe knob is drawn settled at the end of the track instead of sliding across each time the layout reappears.
  • The swipe on the Review sheet now stays visible while the subscription is being accepted; it used to disappear once the proposal turned active.
  • The Review sheet no longer shows the First billing period ends … Each period is charged in full. line, which is not in the Figma frame. It was added in ef50785 to disclose that a short first period is charged in full.
  • The swipe on the Review sheet reads Swipe To Subscribe, as in Figma, whether or not a first payment is due. It used to read Swipe To Subscribe & Pay when one was.
  • The clock illustration on the Review sheet is drawn at the frame's size and 15° rotation. It is the same asset as in Figma, previously shown smaller and unrotated.
  • A hardware wallet still opens the send confirmation, because the device has to sign.
  • The automatic start waits for coin preselection and the fee estimate, so the fee warnings are checked against the coins that are spent. With no fee estimate or no preselected coins it does not start, and the regular send confirmation is shown instead.
  • With manual coin selection the first swipe opens the coin picker, and the regular send confirmation then stays up for a second swipe and while that payment sends.
  • The fee-over-half-the-amount warning no longer depends on a USD rate being available; before, a missing rate skipped it for every send.
  • PIN or biometric confirmation for payments and the existing send warnings (over 50% of balance, fee over 50% of the amount, and the others) still run before the payment is sent.
  • This reverses the on-chain part of d7d7ab0 (fix: confirm initial subscription fees, feat: add Paykit subscriptions #1186): the network fee for the first on-chain payment is no longer shown before it is paid.

Out of Scope

Design

Preview

Pixel 9 emulator, dev build on regtest, payer with on-chain savings only, proposal sent from a second emulator. After: 5,000 sats plus a 141 sat fee left the wallet with one swipe.

Before, after and Figma

Recording of the single swipe on the current base. The wait between the swipe and Subscribed took 83 seconds on the emulator and is sped up 8x; the first and last nine seconds are real time.

subscription_single_swipe_v5.mp4

QA Notes

Journeys

  • updated review-and-subscribe.xml — with the first period due on acceptance and on-chain savings only, one swipe keeps the Review and Subscribe layout with the swipe loading and ends on Subscribed with no second swipe
  • updated cancellation-during-confirmation.xml — the first period is now paid on acceptance, so the journey advances the subscription clock and cancels while the next unpaid period is on the send confirmation; not driven on a device

Manual Tests

N/A

Automated Checks

  • updated AppViewModelSendFlowTest.kt — an initial on-chain subscription payment starts automatically, stops at the fee warning when the fee is over half the amount, checks the fee of the preselected coins, requires confirmation when the fee is unknown, requires confirmation after manual coin selection, shows the fee warning without a USD rate, and a hardware wallet one still requires confirmation
  • updated SendConfirmScreenTest.kt — while the first payment is sent the Review & Subscribe layout stays with a settled swipe, and a bare progress indicator shows only when the subscription is missing
  • ran just compile, just test, just lint — pass locally; SendConfirmScreenTest.kt compiles but was not run on a device

@jvsena42
jvsena42 added this pull request to stack #1423 October 5, 2026 15:53
@jvsena42 jvsena42 changed the title fix/subscription onchain single swipe fix: pay first onchain subscription period in one swipe Oct 5, 2026
@jvsena42 jvsena42 self-assigned this Oct 5, 2026
@jvsena42
jvsena42 marked this pull request as ready for review October 5, 2026 16:27
@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the previous fee-warning issue is fixed and no new blocking issue was found.

Summary

The PR sends the first on-chain subscription payment from the acceptance swipe and keeps the review layout visible while payment runs.

  • On-chain first payments start from the subscription acceptance swipe.
  • The subscription review stays visible while its first payment sends.
  • The subscription review matches its design more closely.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Swipe to subscribe] --> B{First payment due?}
  B -->|No| C[Subscribed]
  B -->|Yes| D{Automatic payment ready?}
  D -->|No| E[Send confirmation]
  D -->|Yes| F[Check payment warnings]
  E --> F
  F --> G[Confirm PIN or biometrics if required]
  G --> H[Send payment]
  H --> C
Loading

Reviews (5) · Last reviewed commit: "fix: check first payment fee ratio witho..." · Reviewed by Greptile

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from f729fab (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@jvsena42 jvsena42 mentioned this pull request Oct 5, 2026
5 of 29 tasks
@jvsena42
jvsena42 marked this pull request as draft October 6, 2026 11:32
@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from 92c274c to 06e58ea Compare October 6, 2026 12:12
@ovitrif
ovitrif force-pushed the fix/subscription-onchain-single-swipe branch from d6855b0 to 3e7f59d Compare October 7, 2026 17:17
@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from 3e7f59d to 650bbe2 Compare October 8, 2026 09:09
@jvsena42
jvsena42 marked this pull request as ready for review October 8, 2026 09:37
@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from 650bbe2 to 0f0b4c3 Compare October 8, 2026 09:47

@talosmachina talosmachina left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No findings. Drops the Lightning-only condition from SendUiState.shouldAutomaticallyPay, so the first on-chain subscription period is paid from the Review swipe, and keeps the Review & Subscribe layout on screen while it sends. Reviewed 0f0b4c3, full tier, reasoned from the code and CI: on the Bitkit track an auto review does not build an Android branch here.

What I checked, and 4 candidates I ruled out

Read in full: SendConfirmScreen.kt (screen and SendConfirmContent), SubscriptionsScreen.kt review path, the SendConfirm route in SendSheet.kt, onStartInitialSubscriptionPayment, onSwipeToPay, handleSanityChecks and acceptSubscriptionAndStartPayment in AppViewModel.kt
CI: CI and Lint green (build, testDevDebugUnitTest, detekt); the e2e shards were still running at review time

Ruled out

  • On-chain auto-pay firing before the fee is known: handleSanityChecks computes the fee itself through lightningRepo.calculateTotalFee and still raises the fee-over-50% and fee-over-$10 warnings, so the on-chain checks run on the automatic path too.
  • Overlay hiding the warning or biometric prompt: AutoPayOverlay sits under BiometricsView and the sanity AppAlertDialog, and dismissing a warning still sends CancelInitialSubscriptionPayment.
  • Hardware wallet paying without the device: shouldAutomaticallyPay still requires hardwareWalletId == null, covered by initial hardware subscription payment requires confirmation.
  • Overlay matching the wrong subscription: the lookup in SendSheet.kt keys on both paymentRequestId and counterparty, the same pair PaykitSubscriptionId uses; no match falls back to the existing spinner.

Merge confidence: 4/5, no findings and the change is covered by AppViewModelSendFlowTest.kt, but the e2e shards were still pending and the branch was not built here.

@jvsena42
jvsena42 requested review from a team, ben-kaufman and coreyphillips and removed request for a team October 8, 2026 10:31
@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from 0f0b4c3 to 0a3c71b Compare October 8, 2026 11:38
@coreyphillips

Copy link
Copy Markdown
Contributor

Two independent reviews, nothing blocking a merge.

worth doing, does not block

  • cancellation-during-confirmation.xml can no longer reach its confirmation step (journeys/subscriptions/cancellation-during-confirmation.xml:10). journeys/subscriptions/cancellation-during-confirmation.xml relies on the on-chain first payment stopping at the send confirmation, and this PR removes that stop. The journey funds the payer on-chain and has the issuer propose a monthly subscription with a fixed P2WPKH endpoint. Step 2 then says to accept it and "open its unpaid period to the send confirmation screen without confirming payment". shouldAutomaticallyPay no longer checks SendMethod.LIGHTNING (AppViewModel.kt:6482). Accepting a period that is due on acceptance therefore starts the payment from the LaunchedEffect at SendConfirmScreen.kt:179, so no unpaid period is ever left on a confirmation screen. Step 5 ("Verify no transaction to the request's receiving address was broadcast") would then fail for a reviewer who drives it. CLAUDE.md asks a PR to update any journey whose route it changes. One way to keep the cancellation window is to propose with a future start and advance the subscription clock offset, as fixed-onchain-destination.xml does. Another is to cancel while a PIN-for-payments prompt is open. Whichever is used, list the updated journey under Journeys. I confirmed this by tracing the code path; I did not drive the journey on a device.
  • Updated androidTest checks only the fallback path, not the overlay that ships (app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreenTest.kt:180). initialOnchainSubscriptionPaysWithoutSecondSwipe renders SendConfirmContent without autoPayContent, so it tests the bare-spinner fallback. Production does not take that path when the subscription is found. In SendSheet, autoPayContent renders SubscriptionReview. Its SwipeToConfirm carries testTag GRAB, so onNodeWithTag("GRAB").assertDoesNotExist() would fail against the real screen. The test passes, but it does not describe what users see. The overlay is the main UI change and has no test, and the PR says this test was not run on a device. The PR also cites the fee warnings as the remaining safeguard for on-chain auto-pay, but nothing in app/src/test covers FEE_OVER_HALF_VALUE or FEE_OVER_10_USD. Two tests would close this: - In the androidTest, pass an autoPayContent and assert the Review title and a non-draggable swipe. - In AppViewModelSendFlowTest, stub calculateTotalFee above half the amount and assert that StartInitialSubscriptionPayment sets showSanityWarningDialog and leaves shouldConfirmPay false.

nits

  • SendSheet collects pubkyContacts a second time and shadows contacts (app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt:339). SendSheet already collects appViewModel.pubkyContacts into contacts at line 116. The Confirm destination collects the same flow again under the same name at line 339, which shadows the outer variable. Reuse the outer contacts. The subscription lookup at lines 340 to 346 duplicates appViewModel.subscription(PaykitSubscriptionId(id.paymentRequestId, id.counterparty)), which already does the same match.
  • Overlay Review layout is not inert: tappable card and the swipe knob slides in (app/src/main/java/to/bitkit/ui/screens/subscriptions/SubscriptionsScreen.kt:764). SubscriptionFirstPaymentProgress passes onDetails = {}. SubscriptionProviderCard still applies clickable, so the card shows a ripple and does nothing. Making onDetails nullable there and passing null would remove the dead tap target. Separately, SwipeToConfirm starts with panX at 0 and springs to the end when confirmed is true. The overlay's knob therefore slides across every time the overlay appears: after the sheet swap, and again after a biometric prompt.

@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from 0a3c71b to bb06844 Compare October 8, 2026 16:04
@jvsena42

jvsena42 commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Went through the review; all four points are addressed.

  • cancellation-during-confirmation.xml: fixed in 2a7c6ba. The first period is paid on acceptance, so the journey now advances the subscription clock offset and cancels while the next unpaid period is on the send confirmation. Listed under Journeys. Not driven on a device.
  • androidTest / fee warnings: fixed in bb06844. SendConfirmScreenTest.kt now renders the real SubscriptionFirstPaymentProgress as autoPayContent and asserts the Review layout with a settled swipe, plus a separate case for the spinner fallback. AppViewModelSendFlowTest.kt gains a case where the fee is over half the amount: the warning dialog shows and the payment does not start. The unit test passes locally; the androidTest compiles and is left to CI.
  • SendSheet duplicate collection: fixed in ca9b200. Reuses the outer contacts and appViewModel.subscription(PaykitSubscriptionId(...)).
  • Overlay not inert: fixed in 5cd53f6. onDetails is nullable and the card has no click target while paying; SwipeToConfirm snaps to the end when it starts confirmed. Checked on a Pixel 9 emulator: the knob is at the end from the first frame through to Subscribed.

coreyphillips
coreyphillips previously approved these changes Oct 8, 2026
@jvsena42
jvsena42 marked this pull request as draft October 12, 2026 17:34
@jvsena42
jvsena42 marked this pull request as ready for review October 12, 2026 17:50

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA review

Scope: Re-reviewed the full PR at d60477987 after the comparison base changed since the saved assessment. No coverage baseline was supplied, so inherited coverage is empty and this pass covers the diff from merge base ff0ca52ee.

No new actionable code findings.

Accepting a subscription starts its first on-chain payment from the Review swipe after coin preselection and a positive fee estimate finish. The fee-ratio warning and the broadcast both use those preselected coins. A missing or zero fee, or automatic selection that produced no coins, leaves the regular send confirmation open. Manual coin selection opens the picker and then keeps that confirmation for a second swipe. A hardware wallet still stops there for signing. The fee-ratio warning runs when no USD rate is available; the over-$100 amount check and the over-$10 fee check still need a rate.

The fee-coin concern on 02420758 is addressed here: the automatic start joins the on-chain refresh job before the sanity checks. The missing-rate skip on this revision's discussion is addressed in d60477987.

Compared with bitkit-ios#881 at 83f77dd4 for this flow. The single swipe, the fee gate, the second swipe after manual coin selection, and the fee-ratio check without a USD rate agree.

CI, including testDevDebugUnitTest, lint, and end-to-end passed on this revision. AppViewModelSendFlowTest.kt and SendConfirmScreenTest.kt were inspected and not executed in this review. Device testing: not performed in this review. The author's manual coin-selection recording was made before the rebase onto this base.

The sheet still closes and reopens during the handoff to payment (#1467), and the payment bell still waits until the on-chain proof completes (#1458). Those paths are unchanged in this diff.

Non-blocking consistency suggestions

The paying overlay keeps the subscription card's chevron when the card has no click handler, so it still looks tappable during the send.

Suggested additional test cases

  • Android, dev regtest, automatic coin selection, payer with on-chain savings only. Accept a subscription whose first on-chain fee is more than half the amount. The fee warning appears before broadcast, and cancelling it does not broadcast.
  • Android, dev regtest, manual coin selection, payer with on-chain savings only and no usable Lightning balance. Accept a 5,000 sat subscription whose first period is due on acceptance. The coin picker opens, the regular confirmation then requires a swipe, the clock review stays away while that payment sends, and the broadcast spends the coins that were selected.

Ready for device testing.

piotr-iohk
piotr-iohk previously approved these changes Oct 12, 2026

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at d6047798 after the current code re-review and regtest device retests.

  • Normal first on-chain subscription payment: one swipe, loading review, then Subscribed; one 5,000-sat payment plus a 209-sat fee.
  • Manual coin selection: picker → choose coins → Continue → regular confirmation → second swipe → the same confirmation stays visible while paying → Subscribed. Logs confirm only the selected 172,945-sat coin was spent, for 5,000 sats plus a 141-sat fee.

The high-fee warning was not exercised on a device. Sheet-handoff flicker and bell-completion timing remain tracked separately in #1467 and #1458.

jvsena42 and others added 18 commits October 12, 2026 16:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from d604779 to f729fab Compare October 12, 2026 19:25
@jvsena42
jvsena42 requested a review from piotr-iohk October 12, 2026 19:26

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved at f729fab8. The subscription payment-code patches are unchanged from the tested d6047798; the rebase adds a journey-document update.

Regtest retests on d6047798 covered normal first on-chain payment with one swipe through Subscribed, and manual coin selection with a second swipe on the regular confirmation. Logs confirmed the manual payment spent only the selected coin.

The sheet-handoff fix now inherited from #1469 has not been device-retested here.

@jvsena42

Copy link
Copy Markdown
Member Author

@piotr-iohk on the non-blocking suggestion that the paying overlay keeps the card's chevron: not changed.

  • The observation is correct. SubscriptionFirstPaymentProgress passes onDetails = null, so the card has no click target, and showAsCard = true still draws the chevron.
  • The overlay exists to keep the Review & Subscribe layout on screen unchanged while the payment sends. Figma has one frame for this screen, Subscribe to Subscription, with the chevron, and no separate paying state. Dropping the chevron mid-send would add a state the design does not have.
  • iOS does the same at f907ff97: the card keeps its chevron while paying and the tap is disabled with allowsHitTesting(!isLoading && !isPaying).

The remaining suggested case (fee above half the amount, warning before broadcast, cancel does not broadcast) is covered by initial onchain subscription payment stops at the fee warning in AppViewModelSendFlowTest.kt. It was not driven on a device.

@jvsena42
jvsena42 merged commit f3bb3d2 into master Oct 13, 2026
21 checks passed
@jvsena42
jvsena42 deleted the fix/subscription-onchain-single-swipe branch October 13, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: first on-chain subscription payment needs a second swipe

4 participants