Skip to content

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

Merged
ovitrif merged 19 commits into
masterfrom
fix/subscription-onchain-single-swipe
Oct 14, 2026
Merged

ovitrif merged 19 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.
  • The automatic start also waits for an on-chain wallet switch to finish choosing coins. Before, a Lightning to on-chain fallback could start the payment while the switch was still running, and the payer saw First Payment Failed. The start that follows the switch runs from an observer of the switch state, so it reads the current setup state and is not cancelled when the switch task ends.
  • Retry from First Payment Failed loads the fee rate again before it reopens the confirmation. Before, the retry cleared the rate and nothing reloaded it, so the Review & Subscribe overlay kept loading. The load runs only while the retry still owns the send, so a retry that outlives its sheet cannot change the fee of a later send.
  • 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
  • added SendConfirmationViewTests.swift — the automatic payment starts only after send setup and an on-chain wallet switch have both finished
  • iPhone 17 simulator at 190d434: Swipe To Subscribe, cancel the PIN prompt, First Payment Failed, Retry Payment, PIN, then Subscribed with no second swipe; the log shows the fee rate reloaded after the retry (proof). The retry has no unit test.
  • added SendConfirmationLifecycleTests.swift — the first payment starts once, uncancelled, when setup finishes during an on-chain wallet switch (fails at 190d434, passes at 608930d)
  • iPhone 17 simulator at 608930d: the retry flow and the one-swipe flow both end on Subscribed with no confirmation screen (proof)
  • ran SendConfirmationLifecycleTests, SendConfirmationViewTests, SendConfirmationSwipeTests and WalletViewModelSendTests at 608930d — 41 of 42 pass; testCustomFeeWalletSwitchSurvivesFeeScreenNavigation fails the same way at 190d434
  • 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: 5/5

[High impact] The PR appears safe to merge; no new blocking issues were found.

Summary

This PR lets the first on-chain subscription payment start from the acceptance swipe. It keeps the review layout visible while payment runs and preserves manual confirmation for hardware wallets and manually chosen coins.

  • Since the last review, payment startup moves outside the wallet-switch task so clearing that task does not cancel the PIN check.
  • Retry now checks that it still owns the send before loading and applying a fee rate.
  • A new lifecycle test covers setup finishing before wallet selection.
  • All five supplied previous threads are addressed in the current code. None supplied a comment number, so previousFindings is empty.
  • No new actionable issues found. This review used code inspection; no tests or device checks were run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Accept subscription] --> B[Load fee rate and choose funding]
    B --> C[Wait for wallet switch]
    C --> D{Funding and coin selection}
    D -->|Savings, automatic coins| E[Check fee warnings and payment PIN]
    D -->|Hardware or manual coins| F[Show manual confirmation]
    E --> G[Send first payment]
    F --> G
    G --> H[Subscribed]
Loading

Reviews (7) · Last reviewed commit: "fix: keep stale retry from setting a new..." · 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
@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.

Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift
@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 thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift

@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.

Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift
jvsena42 and others added 2 commits October 14, 2026 08:48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jvsena42

jvsena42 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

Device proof for the retry fix (190d434), on the iPhone 17 simulator, regtest. The Android issuer proposed 5,000 sats monthly. The iOS payer had on-chain savings only, Autopilot coin selection and PIN required for payments.

Swipe To Subscribe → PIN prompt → cancel → First Payment Failed → Retry Payment → loading review → PIN → Subscribed. One swipe in total.

First Payment Failed Subscribed after Retry

11:58:58.265  Selected fee rate: 1 sats/vbyte for speed: normal   (first attempt)
11:59:21      PIN cancelled → First Payment Failed
11:59:33      Retry Payment tapped
11:59:43.685  Selected fee rate: 1 sats/vbyte for speed: normal   (reloaded by the retry)
12:00:05      PIN entered
12:00:23.982  Onchain send result txid: ea4da829…
12:00:26.986  Balance changed: onchain=3058318->3053175

5,143 sats left savings: the 5,000 sat period plus a 143 sat fee. There is no Opening manual confirmation for first subscription payment line anywhere in the log.

retry_flow_small.mp4

@jvsena42
jvsena42 marked this pull request as ready for review October 14, 2026 12:04
@jvsena42
jvsena42 requested a review from piotr-iohk October 14, 2026 12:04
Comment thread Bitkit/Views/Wallets/Send/SendSheet.swift Outdated
Comment thread Bitkit/Views/Wallets/Send/SendConfirmationView.swift Outdated
jvsena42 and others added 2 commits October 14, 2026 09:22
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 draft October 14, 2026 12:24
@jvsena42

jvsena42 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

Device proof at 608930d on the iPhone 17 simulator, regtest, with Android issuer Gate58. Each proposal was 5,000 sats monthly, paid from on-chain savings with Autopilot coin selection.

Retry after First Payment Failed (PIN required): Swipe To Subscribe → cancel the PIN prompt → First Payment Failed → Retry Payment → PIN → Subscribed. One swipe in total.

run1-retry_small.mp4

First Payment Failed Subscribed after Retry

12:33:39.734  Selected fee rate: 1 sats/vbyte for speed: normal   (first attempt)
12:33:57      PIN cancelled → First Payment Failed
12:34:07      Retry Payment tapped
12:34:30.732  Selected fee rate: 1 sats/vbyte for speed: normal   (reloaded by the retry)
12:34:48      PIN entered
12:35:13.397  Onchain send result txid: 67cdec47…
12:35:14.545  Balance changed: onchain 3053175 -> 3048032

One swipe, no PIN: Swipe To Subscribe → the loading review stays up → Subscribed. No confirmation screen.

run2-oneswipe_small.mp4

Subscribed after one swipe

12:41:52      Swipe To Subscribe
12:42:33.030  Selected fee rate: 1 sats/vbyte for speed: normal
12:43:08.952  Onchain send result txid: c408176b…
12:43:09.876  Balance changed: onchain 3048032 -> 3042889

Each run took 5,143 sats from savings: the 5,000 sat period plus a 143 sat fee. The Android issuer shows Received Bitcoin 5,000 both times. Opening manual confirmation for first subscription payment does not appear anywhere in the iOS log.

Not driven on a device:

  • the retry losing ownership of the send while its fee rate loads
  • a wallet switch during the automatic start

Those are covered by the code and the lifecycle test.

@jvsena42
jvsena42 marked this pull request as ready for review October 14, 2026 12:46
@jvsena42
jvsena42 requested a review from ben-kaufman October 14, 2026 12:46

@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 at 608930d9, covering the entire delta since b2ae7e15, affected payment paths, and the current PR inventory. Inherited from the baseline review: manual coin selection and its confirmation; review layout, label and overlay; payment safeguards; and the two subscription journeys.

No new actionable code findings.

Source verification closes the prior retry stall, wallet-switch start, and stale retry fee concerns. Scoped Android comparison at f729fab8 supports the same preparation gate.

Validation: 21 local standalone Swift checks passed using source-extracted guard and fee-retry methods with mocked transport. Pinned-head CI tests passed, including the new lifecycle test and the custom-fee navigation test reported failing locally. The author reports one-swipe and PIN-cancel/retry success at this head. No native XCTest or journey was executed locally. Device testing: not performed in this review. Embedded retry ownership has source and primitive-test coverage, without a direct hosted retry assertion.

Suggested additional test cases

  • iOS, regtest, on-chain savings, Autopilot and payment PIN enabled: cancel the first-payment PIN, tap Retry with fee loading held, dismiss the subscription sheet, and open another send with a different fee. Release the old fetch; the replacement send must retain its fee, amount and recipient, with no old PIN request, navigation or payment.

@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 608930d based on the current QA review, passing current-head CI, and the author’s one-swipe and PIN-cancel/retry proof. Prior findings are verified closed. I did not independently rerun device testing.

@ovitrif
ovitrif merged commit e40db96 into master Oct 14, 2026
38 checks passed
@ovitrif
ovitrif deleted the fix/subscription-onchain-single-swipe branch October 14, 2026 21:38
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

6 participants