feat: add quickpay daily spend limit - #1159
Conversation
Greptile SummaryThis PR adds a persistent daily spending cap to QuickPay and coordinates reservation, dispatch, pending, and completion state across payment lifecycles.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope. No blocking failure remains.
|
| 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
Reviews (4): Last reviewed commit: "fix: keep quickpay data during fallback ..." | Re-trigger Greptile
0807618 to
2e4a4c0
Compare
32b59a5 to
49ab192
Compare
2e4a4c0 to
7b9ea19
Compare
7b9ea19 to
f10f4e2
Compare
| } | ||
| val paymentHash = event.paymentHash | ||
| activityRepo.handlePaymentEvent(paymentHash) | ||
| val isQuickPay = quickPayRepo.signalCompletion( |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
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
1,3,5,10,50; default5)threshold × multiplierin USD cents; spend rolls forward on the local calendar daysignalCompletion; keep spend on success; release matching-day failure onlyQuickPayRepoowns sessions, in-flight ops, dispatch, and the daily-cap ledger.QuickPaySpendStoreis an internal class in the same file.Out of scope
Preview
slider.mp4
1159-over-daily-cap-confirm-pin.mp4
QA Notes
Manual Tests
$copy updatesregression:Cold start / locked with PIN → scan stays deferred until unlock (fix: secure shop payments and locked links #1158), then follows the rules aboveAutomated Checks
QuickPayRepoTest.kt:reserveBoundsignalCompletionsuccess keeps spendsignalCompletionfailure rolls backsignalCompletionis idempotentQuickPayViewModelTest.kt:AppViewModelSendFlowTest.kt:signalCompletionPaymentFailedstill resolves PendingPendingPaymentRepoTest.kt:SendPendingViewModelTest.ktQuickPaySettingsScreenTest.kt