Skip to content

feat: add quickpay daily spend limit - #1159

Open
ovitrif wants to merge 67 commits into
masterfrom
fix/require-payment-pin-for-quickpay
Open

feat: add quickpay daily spend limit#1159
ovitrif wants to merge 67 commits into
masterfrom
fix/require-payment-pin-for-quickpay

Conversation

@ovitrif

@ovitrif ovitrif commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Closes #1168

iOS port:

QuickPay stays PIN-free under limits (the point of QuickPay). A hard payment-PIN skip would remove that behavior, so this PR bounds auto-pay with a daily spend multiplier instead. Payments above the daily cap open Confirm, where PIN for payments still applies. Lock-time deferral remains in #1158.

Description

  • Keep QuickPay confirmation-free and PIN-free while under the per-tx threshold and daily cap
  • Add a daily limit multiplier on QuickPay settings (1, 3, 5, 10, 50; default 5)
  • Daily cap is threshold × multiplier in USD cents; spend rolls forward on the local calendar day
  • Bind spend to the invoice payment hash before send; settle only through signalCompletion; keep spend on success; release matching-day failure only
  • Over the daily cap, or if reserve fails at pay time → Confirm (replaces QuickPay; payment PIN only if that setting is on)
  • QuickPay success shows the amount with routing fee; Confirm keeps the invoice amount
  • Unlock/lock scan deferral from fix: secure shop payments and locked links #1158
  • Slider: labels included in layout bounds; tap settles from the current value (no jump back to the first on-screen value)
  • Internal architecture: QuickPayRepo owns sessions, in-flight ops, dispatch, and the daily-cap ledger. QuickPaySpendStore is an internal class in the same file.

Out of scope

  • RTL slider thumb offset: same on master, not a new regression
  • Clock or timezone jumps: rollback keeps spend; only a later local date opens a new bucket
  • In-flight spend after process death past midnight: at most one under-count on the new day, no over-count
  • Scanning an invoice that already paid: success shows the invoice amount, without the routing fee. The daily cap still counted it
  • Compose test for the QuickPay fallback snapshot: covered in QuickPayViewModelTest

Preview

QuickPay Settings Over Cap → Confirm + PIN
slider.mp4
1159-over-daily-cap-confirm-pin.mp4
Screenshot

QA Notes

Manual Tests

  • 1. Settings → QuickPay: both sliders visible; tap a different stop snaps there without jumping to the first step; page scrolls when dragging vertically from a thumb; resolved daily $ copy updates
  • 2. Unlocked, under per-tx and daily cap, PIN for payments on → scan / paste a small LN invoice: QuickPay then success (no Confirm / payment PIN); success amount includes routing fee
  • 3. After the daily cap is filled → scan another under-threshold invoice: Confirm; swipe can ask for PIN
  • 4. Pay-time reserve miss / overlapping pay: Confirm replaces QuickPay; back does not return to an idle QuickPay loader
  • 5. Hold-invoice QuickPay that later fails: failure UI; a following under-cap invoice still QuickPays
  • 6. Hold-invoice QuickPay that later succeeds: success amount includes fee; spend counts toward the cap
  • 7. Confirm Lightning send (over threshold or QuickPay off) that settles immediately: success amount is the invoice amount
  • 8. Confirm Lightning send through Pending, then success: success amount is still the invoice amount
  • 9. regression: Cold start / locked with PIN → scan stays deferred until unlock (fix: secure shop payments and locked links #1158), then follows the rules above

Automated Checks

  • Unit tests added in QuickPayRepoTest.kt:
    • daily-cap reserveBound
    • matching-day release
    • conversion failure
    • signalCompletion success keeps spend
    • signalCompletion failure rolls back
    • signalCompletion is idempotent
    • dual-alias settle
    • inlined iOS/Android ledger decode
    • dispatch-error classification
    • duplicate payment with pending LDK does not refund
    • recovered hash after reload
    • concurrent same-hash pay does not fall back to Confirm
    • live dispatched ops stay out of reconcile
    • re-pay of a settled hash does not double-count
    • reservation persists before dispatch
    • detach before dispatch aborts and releases
    • pre-dispatch rejection refunds
    • null payment rows mutate nothing
    • rescan of a live Pending invoice replays Pending
    • live records survive day prune
    • lookup throw leaves spend unchanged
    • zombie rescan after detach timeout replays Pending
    • detachAll does not sweep a session attached after snapshot
  • Unit tests added in QuickPayViewModelTest.kt:
    • session success mapping
    • session pending mapping
    • session error mapping
    • pay forwards to the repo
    • stale-session detach
    • pay re-entry is guarded until attach resets
  • Unit tests modified in AppViewModelSendFlowTest.kt:
    • under-cap QuickPay vs over-cap Confirm
    • pending settlement through signalCompletion
    • open hash routes to QuickPay when cap would skip
    • null-hash PaymentFailed still resolves Pending
    • contact flow wins over an open hash
    • open-hash routing intentionally bypasses the QuickPay-enabled setting
  • Unit tests modified in PendingPaymentRepoTest.kt:
    • late collector does not receive a buffered resolution
  • Unit tests modified in SendPendingViewModelTest.kt
  • Instrumented tests modified: daily-limit slider tags in QuickPaySettingsScreenTest.kt
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a persistent daily spending cap to QuickPay and coordinates reservation, dispatch, pending, and completion state across payment lifecycles.

  • Adds configurable QuickPay daily-limit multipliers and settings UI.
  • Introduces a persistent payment-hash-bound QuickPay ledger with reconciliation.
  • Routes over-cap or failed reservations to the normal confirmation flow.
  • Updates pending-payment resolution and success amount handling.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
app/src/main/java/to/bitkit/repositories/QuickPayRepo.kt Introduces QuickPay sessions, payment dispatch coordination, persistent daily-cap reservations, completion settlement, and LDK reconciliation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Integrates daily-cap eligibility and QuickPay completion outcomes into Lightning request routing and settlement.
app/src/main/java/to/bitkit/repositories/PendingPaymentRepo.kt Replaces buffered resolution delivery with hash-addressable resolution storage and explicit consumption.
app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt Delegates QuickPay payment lifecycle management to QuickPayRepo and maps session events into UI state.
app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt Adds the configurable daily-limit multiplier and resolved daily-limit presentation.
app/src/main/java/to/bitkit/ui/components/Slider.kt Updates slider gesture handling, tap selection, and label-aware layout behavior.
app/src/main/java/to/bitkit/repositories/LightningRepo.kt Adds a pre-dispatch callback and payment lookup support needed by QuickPay reservation and reconciliation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Decoded Lightning request] --> B{QuickPay enabled and per-tx eligible?}
  B -- No --> C[Confirm payment]
  B -- Yes --> D{Daily-cap reservation succeeds?}
  D -- No --> C
  D -- Yes --> E[Persist hash-bound reservation]
  E --> F[Dispatch Lightning payment]
  F --> G{Completion before timeout?}
  G -- Success --> H[Keep daily spend and show success]
  G -- Failure --> I[Release matching-day spend and show error]
  G -- No --> J[Show pending]
  J --> K[Later completion signal]
  K --> H
  K --> I
Loading

Reviews (4): Last reviewed commit: "fix: keep quickpay data during fallback ..." | Re-trigger Greptile

@ovitrif ovitrif changed the title fix: require payment pin for quickpay fix: add quickpay daily spend limit Aug 15, 2026
Comment thread app/src/main/java/to/bitkit/ui/components/Slider.kt Fixed
@ovitrif
ovitrif force-pushed the fix/require-payment-pin-for-quickpay branch from 0807618 to 2e4a4c0 Compare August 15, 2026 18:28
@ovitrif
ovitrif force-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192 Compare August 15, 2026 18:28
@ovitrif ovitrif self-assigned this Aug 15, 2026
Base automatically changed from cursor/fix-shop-quickpay-auth-86ae to master August 15, 2026 23:02
@ovitrif
ovitrif force-pushed the fix/require-payment-pin-for-quickpay branch from 2e4a4c0 to 7b9ea19 Compare August 15, 2026 23:03
ben-kaufman

This comment was marked as resolved.

github-advanced-security[bot]

This comment was marked as resolved.

@ovitrif ovitrif changed the title fix: add quickpay daily spend limit feat: add quickpay daily spend limit Aug 24, 2026
@ovitrif
ovitrif marked this pull request as ready for review August 24, 2026 16:15
@ovitrif
ovitrif requested review from ben-kaufman, jvsena42 and piotr-iohk and removed request for ben-kaufman, jvsena42 and piotr-iohk August 24, 2026 16:48
}
val paymentHash = event.paymentHash
activityRepo.handlePaymentEvent(paymentHash)
val isQuickPay = quickPayRepo.signalCompletion(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

handlePaymentEvent() can throw before this success reaches signalCompletion(). QuickPay no longer watches node events directly, so the operation times out to Pending and the live ledger entry is excluded from reconciliation even though LDK succeeded. Could we call signalCompletion() before the fallible activity sync and add a test where activity sync fails?

paymentRequest: String,
error: Throwable,
) {
if (error is PaymentAbortedBeforeSend) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

payInvoice() can fail before onBeforeSend() runs, for example if the node stops before executeWhenNodeRunning() enters the send block. This operation is still known to be undispatched, but the ambiguous path marks it dispatched, shows Pending, and keeps spend reserved for a payment that was never attempted. Could we release and report the error whenever current.dispatched is still false, with a pre-callback failure test?

}

private fun shouldNotifyPendingResolution(paymentHash: String): Boolean {
if (_quickPayData.value != null) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This suppression assumes non-null QuickPay data means its Send UI is still visible. showSheet() can replace a QuickPay Pending sheet with ConnectionClosed without running the Send cleanup, leaving _quickPayData set after the pending collector is gone. The later settlement is then suppressed here and no screen receives it. Could sheet replacement run the same QuickPay cleanup, with a pending replacement test?

}

private fun closeActiveSendForFailedPayment(paymentHash: String, reason: PaymentFailureReason?): Boolean {
if (_quickPayData.value != null) return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

signalCompletion() already sends this failure to the active QuickPay session, which navigates to the Error screen. Returning false here also lets the caller fall through to PaymentFailedToast, so immediate QuickPay failures show both. Could we suppress the global toast while the QuickPay UI is active, while keeping it for detached operations?

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.

fix: add quickpay daily spend limit

5 participants