Skip to content

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

Open
jvsena42 wants to merge 15 commits into
masterfrom
fix/subscription-onchain-single-swipe
Open

jvsena42 wants to merge 15 commits into
masterfrom
fix/subscription-onchain-single-swipe

Conversation

@jvsena42

@jvsena42 jvsena42 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Closes #920

Stacked on #880. iOS port of synonymdev/bitkit-android#1422.

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, and brings the Review sheet closer to the frame.

Description

  • Before, an on-chain first payment took two swipes: the Review swipe only subscribed, then the send confirmation opened with a second Swipe To Subscribe & Pay. Figma shows one swipe from Review & Subscribe to Subscribed, and its flow notes do not limit that to Lightning.
  • SendConfirmationView.shouldAutomaticallyPay and requiresManualConfirmation no longer require Lightning funding, so the first payment due on acceptance starts by itself for on-chain savings too. A hardware wallet still opens the send confirmation, because the device has to sign.
  • While that first payment is sent, the sheet keeps the Review & Subscribe layout with the swipe in its completed, loading state, for Lightning and on-chain. Before, InitialSubscriptionPaymentProgress replaced it with a bare progress indicator; that remains only as the fallback when the subscription cannot be found.
  • The swipe on the Review sheet stays visible while the subscription is being accepted.
  • The swipe reads Swipe To Subscribe, as in Figma, whether or not a first payment is due.
  • 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.
  • The clock illustration is drawn at the frame's size and 15° rotation.
  • The card on the Review sheet shows the subscription name and one subtitle line, as in Figma. The contact name and truncated pubky lines are removed, along with the SubscriptionCounterparty identifier.
  • SwipeButton gains isConfirmed, which draws the knob at the end of the track for a swipe that was completed on a previous screen.
  • The automatic start waits for the send sheet to finish loading the fee rate and choosing the funding source, and for a calculated on-chain fee. Without a fee rate or a calculated fee it falls back to the manual confirmation, with its fee-rate retry button, instead of sending or failing, so the fee warnings always have a real fee to check and a hardware wallet picked for insufficient savings still gets its confirmation.
  • With manual coin selection and no coins chosen, the automatic start opens the coin picker instead of failing. Once coins are chosen the regular send confirmation is shown for a second swipe. The confirmation that the picker opens checks the chosen coins, so it does not pay by itself.
  • A fee rate that is still loading no longer sends the automatic start to the regular confirmation. Send setup counts as pending until it has run, and a missing rate falls back to the manual confirmation only once the load has failed; otherwise the start runs again when setup finishes. Before, a start that ran before setup had begun took the missing-rate fallback and asked for a second swipe.
  • Each fallback from the automatic start to the regular confirmation logs its reason (Opening manual confirmation for first subscription payment: …).
  • When a deferred Lightning payment falls back to on-chain, the automatic start waits for the hardware funding check, so a hardware wallet that has to cover the amount gets its confirmation.
  • PIN or biometric confirmation for payments and the existing send warnings still run before the payment is sent.
  • The network fee for the first on-chain payment is no longer shown before it is paid.

Out of Scope

Design

Preview

iPhone 17 simulator on regtest, fresh wallet with on-chain savings only, proposal sent from an Android emulator wallet. 5,000 sats plus a 143 sat fee left the wallet with one swipe.

iOS and Figma

Recording of the single swipe on the current head, in real time: just under a minute from the swipe to Subscribed on the simulator. It took about three minutes before the Paykit runtime updates.

ios_subscription_single_swipe_v4.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 SendConfirmationViewTests.swift — an automatic payment requires manual confirmation only for hardware funding
  • added SendConfirmationViewTests.swift — an automatic payment lacks a fee only on-chain with no calculated fee, and manual coin selection is needed only for on-chain savings without selected coins, and manually chosen coins are detected only for on-chain savings with manual selection
  • added SendConfirmationViewTests.swift — an automatic payment waits for the fee rate only while it is loading, not after the load failed; the setup ordering in the views has no unit test and the fix at b2ae7e1 was not driven on a device
  • ran SendConfirmationViewTests, swiftformat --lint and node scripts/validate-translations.js — pass locally
  • iPhone 17 simulator at 38f6550: one swipe with Autopilot coin selection ends on Subscribed; with Manual coin selection the swipe opens the coin picker and the payment completes after a coin is chosen. testCustomFeeWalletSwitchSurvivesFeeScreenNavigation in SendConfirmationLifecycleTests.swift fails locally on this branch and on master alike

@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from fbc5a5b to e69f9ed Compare October 8, 2026 09:11
@jvsena42
jvsena42 marked this pull request as ready for review October 8, 2026 09:37
@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium impact] Changes subscription payment flow to auto-pay first period.

Wait to merge until automatic payment also waits for the wallet switch to finish.

Findings

  1. P1 First payment starts too early ▶

Summary

This PR lets the acceptance swipe send the first subscription payment from on-chain savings and keeps the Review layout visible while it pays.

  • On-chain first payments can start with the subscription swipe.
  • The review sheet stays visible while the first payment is sent.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Lightning falls back to on-chain] --> B[Check funding source]
    A --> C[Choose coins for wallet switch]
    B --> D[Clear funding pending flag]
    D --> E[Start automatic payment]
    C --> F[Clear walletSwitchContext]
    E --> G{Wallet switch finished?}
    G -->|No| H[Show First Payment Failed]
    G -->|Yes| I[Continue payment]
Loading

Reviews (5) · Last reviewed commit: "fix: keep first payment automatic while ..." · Reviewed by Greptile

Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift Outdated

@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. iOS port of synonymdev/bitkit-android#1422: shouldAutomaticallyPay and requiresManualConfirmation no longer require Lightning funding, and InitialSubscriptionPaymentProgress now renders the shared SubscriptionReviewContent with a confirmed swipe. Reviewed e69f9ed, full tier, reasoned from the code and CI: iOS does not build on this box.

What I checked, and 4 candidates I ruled out

Read in full: InitialSubscriptionPaymentProgress.swift, SwipeButton.swift, the new SubscriptionReviewContent and the review route in SubscriptionsView.swift, the automatic-payment path of SendConfirmationView.swift (startAutomaticPaymentIfNeeded, submitPayment, performPayment)
CI: Greptile and change detection green; Run Tests and build-local were still pending at review time

Ruled out

  • On-chain auto-pay with no fee rate: submitPayment calls wallet.setFeeRate when isFeeRateMissing, and a failure there throws into the .failure route of startAutomaticPaymentIfNeeded, so it never sends at an unset rate.
  • Hardware wallet paying without the device: shouldAutomaticallyPay still excludes hwSend.isActive, and requiresManualConfirmation returns true for a hardware payment, as SendConfirmationViewTests asserts.
  • Missing environment for the new @Environment(PaykitPaymentRequestManager.self): both hosts, SendConfirmationView and LnurlPayConfirm, already read the same environment object, so it is in scope wherever the progress view is shown.
  • isConfirmed leaking into other swipes: it defaults to false and only SubscriptionReviewContent sets it, from isPaying.

Merge confidence: 4/5, no findings, but Run Tests and build-local were still pending and the branch was not built here.

@jvsena42 jvsena42 self-assigned this Oct 8, 2026
@jvsena42
jvsena42 added this pull request to stack #895 October 8, 2026 10:33
@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from a58b830 to 8ef8bc1 Compare October 8, 2026 11:40
@jvsena42
jvsena42 requested review from a team, ben-kaufman and coreyphillips and removed request for a team October 8, 2026 11:53
@coreyphillips

Copy link
Copy Markdown
Contributor

Two independent reviews, nothing blocking a merge.

worth doing, does not block

  • Missing fee rate on-chain fails the first payment instead of falling back (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:666). When the fee rate cannot be loaded, an automatic on-chain first payment lands on the First Payment Failed screen. The PR says it should fall back to the manual confirmation. submitPayment(isAutomatic:) (SendConfirmationView.swift:664-675) calls try await wallet.setFeeRate(...) whenever isFeeRateMissing is true. That call comes before the new automaticPaymentLacksFee check, and setFeeRate throws AppError("Fees unavailable from bitkit-core") when there are no fresh or cached estimates (WalletViewModel.swift:648). This is exactly the state after loadFeeRateWithRetry gives up in SendSheet.setUpSend. The throw reaches startAutomaticPaymentIfNeeded, which pushes .failure. The user is already subscribed and is told the first payment failed. Before this PR, on-chain went to the manual confirmation, which shows the SendConfirmRetryFeeRate button. I confirmed this by reading the code path, not on a device. A fix is to check isAutomatic && isFeeRateMissing and call showManualConfirmation() before the setFeeRate call.
  • Review sheet no longer shows who the subscription is from before a one-swipe payment (Bitkit/Views/Subscriptions/SubscriptionsView.swift:991). After this PR, the Review and Subscribe card shows only fields the payee chose. The title is subscription.note. The avatar is metadata.iconURI whenever the payee sets one (SubscriptionAvatar). The contact name and truncated pubky lines are gone, and the details screen does not show the counterparty either. Before this PR, an on-chain first payment still opened the send confirmation, which shows the contact as recipient. Now one swipe both accepts and pays, on Lightning or on-chain, and at no point is the payer shown who they are paying. This matches the Figma frame, so it may be intended. Still, a proposal with the note "Netflix" and a borrowed icon looks the same as a real one. I could not confirm whether Paykit only accepts proposals from established contacts, which is why this is not blocking. Consider keeping a counterparty line on the card or on the details screen.
  • Fee-rate outages bypass the manual retry screen (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:666). An automatic on-chain first payment opens the failure screen when fee estimates are unavailable instead of exposing the existing manual retry. SendSheet catches the initial fee load error and clears isSetupPending, then this second setFeeRate attempt throws into startAutomaticPaymentIfNeeded, which appends .failure. Before this branch, the manual confirmation remained open with SendConfirmRetryFeeRate.
  • Journey does not force the on-chain payment path (journeys/subscriptions/review-and-subscribe.xml:17). The journey only requires an unspecified spendable balance, so it can select Lightning and pass without exercising the new on-chain single-swipe path. Require on-chain savings above the payment plus fee, with no usable Lightning balance, to make this journey cover the behavior claimed by the pull request.

nits

  • Changelog fragment is not named after the PR number (changelog.d/next/subscription-onchain-single-swipe.changed.md). The fragment is named changelog.d/next/subscription-onchain-single-swipe.changed.md. AGENTS.md asks for <issue-or-pr>.<category>.md, which here would be 881.changed.md.
  • Journey still describes a creator contact card on the review sheet (journeys/subscriptions/review-and-subscribe.xml:22). review-and-subscribe.xml still says "Verify the Review and Subscribe sheet appears with 5,000 sats and the creator contact card". The card now shows only the subscription name and cadence, so the step should describe the subscription card.

coreyphillips
coreyphillips previously approved these changes Oct 8, 2026
@jvsena42
jvsena42 force-pushed the fix/subscription-onchain-single-swipe branch from 8ef8bc1 to d3a9962 Compare October 8, 2026 16:08
@jvsena42

jvsena42 commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Went through the review.

  • Missing fee rate fails the first payment (both entries): fixed in dae43af. An automatic payment with no fee rate opens the manual confirmation, with SendConfirmRetryFeeRate, before setFeeRate is called. No unit test was added for this branch, and the outage was not reproduced on a device.
  • Journey does not force on-chain: fixed in d3a9962. review-and-subscribe.xml now requires on-chain savings above the payment plus fee and no usable Lightning balance.
  • Journey wording / changelog name: fixed in d3a9962 and c327c29. The step describes the subscription card; the fragment is 881.changed.md.
  • cancellation-during-confirmation.xml relied on the on-chain first payment stopping at the confirmation, as flagged on the Android PR. It is updated in d3a9962 to cancel on the next unpaid period. Not driven on a device.
  • Review sheet no longer shows the counterparty: not changed. The card follows the Figma frame, which has the subscription name and one subtitle line only. Paykit delivers proposals only from contacts both sides have saved, so the payer has already added the sender, but the review screen itself does not name them. Adding a counterparty line would diverge from the frame, so it is left as a design decision.

@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. Re-review of the diff since e69f9ed: the branch was rebased onto the new base, and the four original commits are unchanged (git range-diff), so the delta is 1d5cffd9, 177782d8, 9935eb07 and the journey and changelog commits. Reviewed 51e143a, full tier, reasoned from the code and CI: iOS does not build on this box.

What I checked, and 4 candidates I ruled out

Read in full: setUpSend and cleanup in SendSheet.swift (the new isSetupPending), in SendConfirmationView.swift the .task and the new .onChange(of: isSetupPending), startAutomaticPaymentIfNeeded, submitPayment(isAutomatic:), settleTransactionFee, calculateTransactionFee, automaticPaymentLacksFee and showManualConfirmation, the SubscriptionProviderCard change, and both subscription journeys
Earlier findings: I had none. Greptile's two P1s on e69f9ed are fixed in the code. The automatic start now waits for isSetupPending to clear, which happens after selectHardwareFundingSourceIfNeeded. The fee warnings now run after settleTransactionFee(), and a zero on-chain fee falls back to manual confirmation
@coreyphillips's missing-fee-rate point: submitPayment now calls showManualConfirmation() before setFeeRate when isFeeRateMissing, so an outage no longer lands on First Payment Failed
CI: validate and change detection green; Run Tests, Run Integration Tests and build-local still pending at review time

Ruled out

  • Automatic payment re-firing after the manual fallback: showManualConfirmation sets requiresPaymentConfirmation, which makes shouldAutomaticallyPay false, so the isSetupPending change and later fee updates cannot restart it.
  • isSetupPending stuck true: the defer skips the reset only when the task was cancelled. Both cancel sites either start a new task that sets it again (setUpSend) or tear the sheet down (cleanup).
  • settleTransactionFee never converging: after three tries it returns with whatever fee it has, and a fee of 0 on-chain goes to manual confirmation rather than sending.
  • Hardware wallet picked after the decision: the .task sees isSetupPending and does not start; the onChange re-evaluates shouldAutomaticallyPay with hwSend already active and opens the manual confirmation.

Merge confidence: 4/5, no findings, but the fee-rate fallback has no unit test (the PR says so), the test and build jobs were still pending, and the branch was not built here.

coreyphillips
coreyphillips previously approved these changes Oct 9, 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.

QA review

Scope: Reviewed the full PR diff against its merge base, at 51e143ab. There was no saved baseline. Android bitkit-android#1422 at 02420758 was compared only for this first-subscription payment, not reviewed as a whole.

2 actionable findings — resolve or provide an evidence-backed rebuttal.

The fee-rate fallback and the wait for hardware selection when the payment is already on-chain both hold in this revision. Two gaps remain where that automatic path still does not match a manual send or the Android port: manual coin selection, and hardware selection after Lightning is deferred and later falls back to on-chain.

Run Tests, Run Integration Tests, and build-local passed on this revision. This review inspected SendConfirmationViewTests and did not run them locally. Device testing: not performed in this review.

Suggested additional test cases

  • iOS. Advanced coin selection set to Manual. On-chain savings cover a 5,000 sat subscription plus fee, with no usable Lightning balance. Accept a proposal whose first period is due. The coin picker opens, and First Payment Failed does not. After coins are chosen, the payment sends and Subscribed appears.
  • iOS. Lightning channels exist but none are usable. Hot savings are below the amount, and a paired hardware wallet can cover the amount plus fee. Accept the same proposal. The hardware send confirmation opens, and First Payment Failed does not. Retry while those channels stay unusable does not repeat the failure.
  • iOS. On-chain savings only, with fee estimates unavailable. Accept the proposal. The manual confirmation opens with SendConfirmRetryFeeRate, and First Payment Failed does not.

Findings

  • [MEDIUM] Manual coin selection ends on First Payment Failed — inline at Bitkit/Views/Wallets/Send/SendConfirmationView.swift:195.
  • [MEDIUM] Deferred Lightning fallback skips the hardware wallet — inline at Bitkit/Views/Wallets/Send/SendSheet.swift:384.

Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendSheet.swift
Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift
@jvsena42
jvsena42 marked this pull request as draft October 12, 2026 17:34
@jvsena42

jvsena42 commented Oct 12, 2026 •

Copy link
Copy Markdown
Member Author

Manual coin selection after 83f77dd (iPhone 17 simulator, regtest, savings only, 5,000 sat proposal): swipe → coin picker → choose a coin → Continue → regular confirmation waiting for Swipe To Subscribe & Pay → swipe → Subscribed.

17:47:10.360  Opening manual confirmation for first subscription payment: manual coin selection
17:48:03.585  Opening manual confirmation for first subscription payment: not automatic when opened
17:48:41.774  Onchain send result txid: 68e7dbfc…

confirmation after choosing a coin

ios_manual_coins.mp4

@jvsena42
jvsena42 marked this pull request as ready for review October 12, 2026 17:50
@piotr-iohk

piotr-iohk commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Retested on current head 83f77dd4 using the native regtest build on the iPhone 17 Pro simulator. A subscription proposal from Android andi2 to Ios still requires two swipes: the first accepts the subscription, then a detailed payment confirmation appears and requires a second swipe. No manual coin picker was used; the logs confirm default coin selection. The second swipe completes payment and the issuer receives 5,000 sats.

The new diagnostic logs identify the fallback trigger for this reproduction (UTC):

19:12:14.723 Opening manual confirmation for first subscription payment: fee rate missing
19:12:14.856 Selected fee rate: 1 sats/vbyte for speed: normal
19:12:14.879 Calculated transaction fee: 212sats
19:12:16.594 Opening manual confirmation for first subscription payment: not automatic after setup

The fee rate becomes available 133 ms after the fallback. In SendConfirmationView, the missing-rate branch calls showManualConfirmation, which sets requiresPaymentConfirmation = true. Automatic payment remains disabled after the rate arrives, so transient fee loading leaves this normal flow requiring a second swipe.

Please keep the automatic flow waiting while fee setup is still in progress, reserving manual fallback for a genuine failure or a case that requires another confirmation. This normal one-swipe case remains failing on the current head; I am holding iOS approval pending a fix and retest.

Recording of this retest:

ios-double-swipe.mp4

ios-double-swipe-log-excerpts.zip

jvsena42 and others added 13 commits October 12, 2026 16:19
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 83f77dd to f907ff9 Compare October 12, 2026 19:21

@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 f907ff97 after the comparison base changed since the saved assessment. No prior coverage was inherited.

1 actionable finding — resolve or provide an evidence-backed rebuttal.

A normal on-chain savings payment can still leave the acceptance swipe and open the detailed confirmation while the fee rate is loading. SendConfirmationView.swift and SendSheet.swift are unchanged from 83f77dd4, where that second swipe was recorded. Choosing coins still opens the picker, and the confirmation after that choice waits for its own swipe. Android bitkit-android#1422 at f729fab8 was compared only for this first payment: awaitAutomaticOnchainFee waits for the in-flight fee refresh before it requires confirmation.

Run Tests and Run Integration Tests passed on this revision. This review read SendConfirmationViewTests and did not run them. Device testing: not performed in this review.

Suggested additional test cases

  • iOS. Lightning channels exist and none are usable. Savings cannot cover 5,000 sats plus the fee, and a paired hardware wallet can. Accept a proposal whose first period is due. The hardware confirmation opens, and no savings transaction is broadcast before the device signs.
  • iOS. On-chain savings only, after fee estimates have failed and are not still loading. Accept the same proposal. The manual confirmation opens with SendConfirmRetryFeeRate. A fee rate that is only slow to arrive stays on the loading review.

Findings

  • [MEDIUM] On-chain subscription payment asks for a second swipe while the fee is still loading — inline at Bitkit/Views/Wallets/Send/SendConfirmationView.swift:786.

private func submitPayment(isAutomatic: Bool = false) async throws {
guard preparingRequest == nil, walletSwitchContext == nil else { throw CancellationError() }
if isAutomatic, isFeeRateMissing {
showManualConfirmation(reason: "fee rate missing")

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.

[MEDIUM] On-chain subscription payment asks for a second swipe while the fee is still loading

Trigger: accept a subscription whose first period is paid from on-chain savings with Autopilot coin selection, while the send sheet has not finished selecting a fee rate.

isSetupPending starts false, and setUpSend sets it true only after the first render. startAutomaticPaymentIfNeeded treats false as permission to decide. The confirmation .task uses preparingRequest?.id as its identity, so it keeps running with the isSetupPending value from the render that started it. It can enter submitPayment while loadFeeRateWithRetry is still in flight. There, a nil on-chain rate takes the fee rate missing fallback. The swipe loading state treats that same nil rate as loading until feeRateLoadFailed is set; this fallback does not. showManualConfirmation sets requiresPaymentConfirmation, so when setup later finishes and the rate is present, onChange finds automatic payment already disabled and logs not automatic after setup.

The acceptance swipe ends on the detailed savings confirmation, and a second swipe is required. The subscription is already accepted. In the reported run the second swipe paid 5,000 sats plus the fee once.

Expected: keep waiting while fee setup is in progress, including funding selection. Open the manual confirmation when the load has failed or finished without a rate, or when hardware signing, manual coins, or a fee warning requires another confirmation. A normal savings payment should reach Subscribed from the original swipe.

Evidence basis: source analysis at f907ff9. Those two send files match 83f77dd, where the simulator log shows fee rate missing at 19:12:14.723 and Selected fee rate 133 ms later, then not automatic after setup (comment). This review did not run that path. SendConfirmationViewTests covers the fee and coin predicates and does not cover this setup ordering.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 3cfc918 and b2ae7e1. Your fee rate missing → Selected fee rate → not automatic after setup log on 83f77dd matches the code path.

Cause. isSetupPending is a plain property of SendConfirmationView, so the .task that starts the automatic payment reads the value from the render that created it. SendSheet only set it to true inside setUpSend, which runs from its own .task after the first render, so that first render carried false and the guard in startAutomaticPaymentIfNeeded passed while the fee rate was still loading. submitPayment then took the missing-rate fallback, which sets requiresPaymentConfirmation for good.

Changes.

  • 3cfc918: isSetupPending starts true in SendSheet, so a confirmation rendered before setUpSend runs does not read setup as finished. The automatic start then comes from onChange(of: isSetupPending) once the fee rate has loaded and hardware funding selection is done.
  • b2ae7e1: in submitPayment, an automatic payment with a missing rate opens the manual confirmation only when wallet.feeRateLoadFailed is set. While the rate is loading it returns without disabling automatic payment, and the start runs again when setup finishes. This is the same missing-and-not-failed condition the swipe already treats as loading.

A failed fee load still opens the manual confirmation with SendConfirmRetryFeeRate: setup ends, the start runs, the rate is missing and feeRateLoadFailed is true. Hardware wallet, manual coins and the fee warning are unchanged.

Test. testAutomaticPaymentAwaitsFeeRateOnlyWhileItIsLoading in SendConfirmationViewTests.swift. 31 tests across SendConfirmationViewTests, SendConfirmationSwipeTests and WalletViewModelSendTests pass on the iPhone 17 simulator (checked in the xcresult that the new case executed). It pins the wait/fallback decision only. The render ordering lives in the views and has no unit test.

Not verified on a device. I did not drive the flow this round: it needs the Android issuer, and the emulators are in use. I also never reproduced the second swipe locally, so a passing run of mine would not prove much. Leaving this thread open for your retest; the expected log after one swipe has no Opening manual confirmation line.

Android. bitkit-android#1422 does not have this: awaitAutomaticOnchainFee joins the in-flight fee refresh before it decides.

@jvsena42
jvsena42 marked this pull request as draft October 13, 2026 10:52
jvsena42 and others added 2 commits October 13, 2026 07:55
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jvsena42
jvsena42 marked this pull request as ready for review October 13, 2026 10:55
@jvsena42
jvsena42 requested a review from piotr-iohk October 13, 2026 10:55
Comment on lines +397 to +403
.onChange(of: isSetupPending) { _, isPending in
guard !isPending, preparingRequest == nil else { return }
Task { @MainActor in
if app.contactPaymentContext?.isInitialSubscriptionPayment == true, !shouldAutomaticallyPay {
showManualConfirmation(reason: "not automatic after setup")
}
await startAutomaticPaymentIfNeeded()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 First payment starts too early

When an initial subscription payment falls back from Lightning to on-chain while confirmation is already open, funding selection and onSwitchToOnchainWallet run separately. Funding selection can finish while the wallet switch is still choosing coins. Clearing isSetupPending then starts automatic payment, but submitPayment throws because walletSwitchContext is still set. The user sees First Payment Failed even though setup is simply unfinished.

Wait for both tasks before starting payment, and retry the automatic start when the wallet switch finishes.

@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: Follow-up review of the changes since f907ff97, at b2ae7e15. Baseline: previous QA review. This pass rechecked the initial on-chain fee wait, the setup-pending flag, the new predicate test, and Android's fee wait at f729fab8. Inherited coverage: manual coin selection and the confirmation after coins are chosen; review sheet layout, swipe label, and in-payment overlay; payment warnings, PIN, and hardware confirmation on the automatic path; journeys review-and-subscribe and cancellation-during-confirmation.

1 actionable finding — resolve or provide an evidence-backed rebuttal.

The second-swipe report is closed for the first setup. isSetupPending starts true, and a missing rate opens the manual confirmation only after feeRateLoadFailed is set. That matches Android awaitAutomaticOnchainFee, which joins the in-flight fee refresh before it decides. Run Tests passed on this revision. This review read the new predicate and did not run it. Device testing: not performed in this review.

Retry after First Payment Failed clears the fee rate and leaves the automatic start waiting for a setup pass that does not run again. The open wallet-switch comment still matches this head: clearing the funding wait can start payment while walletSwitchContext is set, and that cancellation opens First Payment Failed. These two commits leave that path as it was.

Suggested additional test cases

  • iOS simulator, regtest, on-chain savings only, Autopilot coin selection. Accept a proposal whose first period is due while fee estimates are slow. One swipe keeps the review layout loading, the log has no Opening manual confirmation for first subscription payment, and Subscribed appears.
  • iOS simulator, regtest, on-chain savings only, with payment PIN enabled. Accept the same proposal, cancel the PIN prompt so First Payment Failed appears, then tap Retry. The fee rate loads again and the payment continues, or the manual confirmation shows SendConfirmRetryFeeRate and can complete. The review overlay does not remain loading.

Findings

  • [MEDIUM] Retry leaves the first payment waiting after the fee rate is cleared — inline at Bitkit/Views/Wallets/Send/SendConfirmationView.swift:788.

if isAutomatic, isFeeRateMissing {
guard !Self.automaticPaymentAwaitsFeeRate(isFeeRateMissing: isFeeRateMissing, feeRateLoadFailed: wallet.feeRateLoadFailed) else {
// The rate is still loading; the automatic start runs again when send setup finishes.
hasStartedAutomaticPayment = false

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.

[MEDIUM] Retry leaves the first payment waiting after the fee rate is cleared

Trigger: the first on-chain subscription payment reaches First Payment Failed, including when the payer cancels the PIN prompt or a fee warning, and they tap Retry.

Mechanism: retryEmbeddedInitialSubscriptionPayment calls wallet.resetSendState, which sets the selected fee rate to nil and feeRateLoadFailed to false. It does not run setUpSend again, so isSetupPending stays false. The recreated confirmation calls startAutomaticPaymentIfNeeded. automaticPaymentAwaitsFeeRate is then true, and this branch clears hasStartedAutomaticPayment and returns. The restart path is onChange(of: isSetupPending) when setup finishes. Setup does not run again, and the fee-rate onChange only recalculates the fee. testAutomaticPaymentAwaitsFeeRateOnlyWhileItIsLoading checks the predicate and does not cover a completed setup that will not run again.

Consequence: the Review & Subscribe overlay stays in its loading state. requiresPaymentConfirmation stays false, so the manual confirmation and SendConfirmRetryFeeRate stay hidden. The accepted subscription's first payment does not send from this retry. The payer can dismiss the sheet and pay that period later from the payment-due notification.

Expected: retry loads the fee rate again, or a missing rate with no load in progress opens the manual confirmation and its fee retry.

Evidence basis: source analysis at b2ae7e15. This review did not run the retry path.

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