Repository navigation
Conversation
An Allowance now binds two identities, so a grant is one SDK Allowance on the contact's single Encrypted Link and coverage no longer depends on a receiver folder. Automatic payments claim the request before the automatic Acceptance, pass the payee's payment app on the proof and settle the private payment list with the same outcomes the manual flow uses.
A cancelled caller no longer stops an automatic payment after the request was accepted: admission runs in its own task once the ledger is reconciled.
The shared request flow only shows an accepted request again while this device owns its acceptance. Automatic payments now record that ownership before the automatic Acceptance, so a payment that never reached the node returns as an ordinary Payment Request.
…l-20260930' into feat/paykit-allowances
1a0051f to
8f0449c
Compare
|
I restacked this PR on #856 and pushed 8f0449c (draft, base
Local verification: |
…yment is decided A request an allowance covers belongs to the automatic flow from the moment it arrives. Before, a slow first pass let the incoming-request presentation open a manual sheet for a request that was then paid automatically.
…owance flow owns Skipping an owned request left it in the presentable list, so the presentation re-entered itself on the main thread until the allowance pass finished. The pass now logs how each covered request ended.
…l-20260930' into feat/paykit-allowances
|
I pushed a19ea87 with the device findings and the Preview media.
Device runs on two iOS 26.5 simulators against Paykit |
|
I ran the open checks on the current head (a19ea87), on two iOS 26.5 simulators against Paykit
The journeys are ticked in QA Notes; the manual test list is now N/A. |
Tapping Pay on a request the allowance flow holds shows that it is already being paid, as on Android, and recovery announces only payments no settlement event announced.
|
@coreyphillips, I've addressed each point in fd4c701. The allowance, payment request and allowance executor unit tests pass on that head, including new regression tests for 1, 3, 4 and 6.
Fixed in da90209. While no channel is usable, each covered request is now checked against its contact's allowance limits before the wait. A request that fits none of them is handed back for manual payment, raises Limit Reached, and opens its Payment Request sheet. Requests within the limits still wait for the channel.
Fixed in da90209 and 7cb14fb. The skip branch now ends the requested presentation, so other sheets and proposals present again. A Pay tap on a held request shows "This request is already being paid." instead of a spinner. This matches the Android twin.
Fixed both halves in da90209:
Fixed in da90209 and 7cb14fb. When recovery settles an automatic payment, it now raises the same Payment Executed event that links the contact and posts the notification. It does so only when no settlement event already announced that payment, so nothing fires twice (
Fixed in da90209. An error from
Fixed in fd4c701. All 47 allowance strings are now translated in all 14 locales. Each locale reuses its file's existing terms and tone, and keeps every placeholder. On the Android review's point that a settled automatic payment was treated as the user's own send: iOS doesn't have that gap. The Send sheet and the payment toasts resolve only for payments the user sent from the foreground ( |
A reservation blocked for another reason, such as an ended allowance or a manual-only decision, sends the request to manual payment without the Limit Reached notification, as on Android.
|
I pushed 3e4c612, matching the Android twin synonymdev/bitkit-android#1340: Limit Reached now appears only when a reservation is blocked by an amount, count or period limit. A request blocked for another reason, such as an ended allowance or a manual-only decision, still goes to manual payment but without the Limit Reached notification. A new test covers it, and the allowance unit tests pass on this head. |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 4 new commits (da90209, 7cb14fb, fd4c701, 3e4c612). One point still blocks a merge.
addressed
- Pay on a held covered request stalls all Paykit presentation (Bitkit/AppScene.swift:1590). Fixed:
AppScene.swift:1596-1599now toasts and callsreleaseRequestedPresentation(PaykitPaymentRequestService.swift:2198-2204), which clearsrequestedPresentationId. That reopensrequestsForPresentation(),requestPresentationandpresentNextItem, and stops the Pay spinner. I traced this but did not run it, since this machine has no Xcode. Other sheets come back on the next poll, becauseshouldPresentNextRequeststays false. - New allowance strings are missing from the 14 non-English locales (Bitkit/Resources/Localization/en.lproj/Localizable.strings). Fixed: All 47
subscriptions__allowance*keys are now in each of the 14 locales.validate-translations.jsreports 0 errors and lists no missing allowance key. A script comparing{placeholder}sets against English found no mismatches, andplutil -lintpasses on every file. - Execution startup errors leave unsent attempts reserved (Bitkit/Services/PaykitAllowanceExecutor.swift:597). Fixed: If
beginPaymentExecutionthrows,PaykitAllowanceExecutor.swift:647-657now records.failedbefore rethrowing.recordreports to the SDK and moves the journal entry to.failed, andPaykitAllowanceCapacity.attemptsonly counts attempts whose status is not.failed, so the capacity is freed. Covered bytestExecutionStartupErrorReleasesTheReservation. - Allowance screens and notifications lack translations (Bitkit/Resources/Localization/en.lproj/Localizable.strings:1778). Fixed: Same as point 4: all 14 locales now carry the 47 keys, with placeholders intact. I confirmed this with the validator, a placeholder comparison and
plutil -lint.
still open
- Request arriving with the acceptance opens a manual sheet, then auto-pays (Bitkit/AppScene.swift:1424). A request that arrives in the same intake as the payee's acceptance can open as a manual Payment Request and then be paid automatically underneath it. In
refreshIncomingPaykitPaymentRequests, the payment request refresh receives the link's messages and updatespendingRequests. That fires the.onChangeatAppScene.swift:716, which startspresentNextIncomingPaykitItem()in a Task. The Task can run whilepaykitAllowanceManager.refresh()is suspended at line 1424. In that caseisAutomaticallyHandlingcheckscoversRequestagainst allowances read before the intake, where the allowance is still proposed. The Send sheet opens.processIncomingRequeststhen sees the refreshed allowance and auto-pays the same request. This happens when the payer's app was closed while the payee accepted and sent a request. No second payment goes out: the consumed list version, the proof and the ledger each stop it. But the user's swipe fails. If the swipe wins the race instead, the executor's refused acceptance callsdiscardAcceptanceIntent. That removes the id the manual flow saved in the shared acceptance store.prepareForPaymentguards against this withalreadySaved; the executor does not. This comes from code reading; I did not reproduce it. Codex still sees it: The intent ownership guard is fixed at Bitkit/Services/PaykitAllowanceExecutor.swift:600, but Bitkit/AppScene.swift:1426 only excludes sheets already open at that instant. An isolated scheduling test still opened a manual sheet during automatic execution whenisAutomaticallyHandlingsuspended at Bitkit/Services/PaykitAllowanceManager.swift:328 across the allowance refresh. - Payments settled by restart recovery get no contact or notification (Bitkit/Services/PaykitAllowanceExecutor.swift:368). An automatic payment that restart recovery marks succeeded is never linked to the contact and raises no Payment Executed notification.
recover(identity:)records outcomes throughrecord(...), which only emits.ledgerChanged. The.paidAutomaticallyevent drives both the notification andactivity.setContactinhandlePaykitAllowanceEvent. OnlylightningPaymentSettledsends it, and that function returns early whileactiveIdentityis nil (line 689) and skips entries already at.succeeded. So after a kill and relaunch, the Activity row gets no contact if the node reports the payment's success before the allowance manager activates, or beforerecoverreads the node. The Auto-paid label still shows because it reads the journal. Sending.paidAutomaticallywhen recovery moves an automatic entry to succeeded would close the gap. Codex still sees it: Reconciliation at Bitkit/Services/PaykitAllowanceExecutor.swift:376 can settle an attempt before the recovery loop, which skips.succeededat line 395. Running those recovery methods in isolation with that response produced no payment event and left the journal at.sent, so this path still misses the contact and notification. - Channel reconnection hides requests that require manual approval (Bitkit/Services/PaykitAllowanceManager.swift:290). An above-limit request cannot open its normal Payment Request sheet while channels are unusable. With a 1,000-sat allowance, a 2,000-sat request and
canPayNow == false, I confirmed no eligibility check runs andisAutomaticallyHandlingremains true.AppSceneconsequently suppresses presentation even after the user taps Pay. Determine which requests need manual approval before applying the channel wait. Codex still sees it: The preflight at Bitkit/Services/PaykitAllowanceExecutor.swift:426 still includes expired allowances. An isolated run with an active 1,000-sat grant, an expired 10,000-sat grant and a 2,000-sat request returned false fromexceedsLimits, so Bitkit/Services/PaykitAllowanceManager.swift:293 still holds a request requiring manual approval.
new since the last review
worth doing, does not block
- Recovery and a settlement event can both announce one automatic payment (Bitkit/Services/PaykitAllowanceExecutor.swift:389). Recovery and the node's settlement event can each announce the same payment, which posts two Payment Executed notifications.
recoverreadswasSettledfrom the journal atPaykitAllowanceExecutor.swift:389, then awaitsrecord, which suspends insidesdk.recordPaymentOutcomebefore it updates the journal (:476-487). IflightningPaymentSettledruns during that suspension, it still sees the entry at.sent(:747-750), records it and sends.paidAutomatically(:754-761). Recovery then resumes and announces too (:392-393). This is the restart case:activatesetsactiveIdentityjust beforerecover, and LDK replays thepaymentSuccessfulevent around node start. Whether both fire depends on the SDK accepting a second identical outcome report; if it throws instead, the event path only logs a warning. I found this by reading the code and did not reproduce it. A cheap fix is to haverecordreport whether itsupdateLocalStateclosure moved the entry to.succeeded, and announce only from the caller that made that move. - Recovery and settlement can notify twice for one payment (Bitkit/Services/PaykitAllowanceExecutor.swift:389). A settlement arriving during recovery can raise Payment Executed twice.
wasSettledis captured beforerecordsuspends, solightningPaymentSettledcan also capture the unsettled journal entry and both paths announce it. I reproduced two.paidAutomaticallyevents in isolation by pausing the outcome save and delivering settlement before it completed.
No manual request sheet opens while an allowance refresh or pass may still take the request, the limit check ignores expired allowances, and Payment Executed is raised once, from whichever path settles the journal entry, including payments reconciliation settled before recovery.
|
@coreyphillips, efe4aa2 addresses the three points still open from your recheck, plus the new double-announce finding. The allowance, payment request and allowance executor unit tests pass on that head.
While an allowance refresh or automatic pass is running, the presentation loop now opens no sheet for a request from a contact this wallet grants, or was asked to grant, an allowance. That holds even after
Payment Executed is now raised in one place: the journal update that moves an automatic entry to succeeded. Settlement, recovery, and attempts that reconciliation already settled all go through it. The recovery loop now also moves the journal for an attempt the SDK already shows as succeeded, so the case Codex ran (reconciled before the loop) links the contact and notifies (
Fixed with the same change: only the call that moves the entry to succeeded announces. The journal update runs inside the executor actor with no suspension, so a settlement and recovery can't both see the entry unsettled ( |
The journal keeps the list version an automatic attempt consumed, and recording the attempt failed releases it, after a restart too, so the manual payment that follows can use the same list. Pay on a request the allowance flow holds keeps the requests sheet open, as on Android.
|
I pushed 69da189, matching two changes on the Android twin synonymdev/bitkit-android#1340:
Two new tests cover the release, and the allowance, payment request and private Paykit unit tests pass on this head. |
…iled Recovery moves the journal of attempts the SDK already settled as failed too, and releases the payee's list they consumed, as on Android.
|
I pushed 6f74f1a, matching the Android twin synonymdev/bitkit-android#1340. Restart recovery now also updates the journal for an attempt the SDK's reconciliation already settled as failed, and releases the payee's payment list it consumed. A new test covers it, and the allowance unit tests pass on this head. |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 3 new commits (efe4aa2, 69da189, 6f74f1a). Nothing blocking remains.
addressed
- Request arriving with the acceptance opens a manual sheet, then auto-pays (Bitkit/AppScene.swift:1424). Fixed: The guard at
AppScene.swift:1604runs synchronously right beforeshowSheetat :1615, andupdateDepthgoes up before the first await inrefresh()andprocessIncomingRequests(PaykitAllowanceManager.swift:149, :296), so no window is left open. A sheet opened before the pass is still excluded atAppScene.swift:1426, andrecordedAcceptanceIntent(PaykitAllowanceExecutor.swift:630, :644) keeps the manual intent. Confirmed by reading;testManagerMayCoverAContactItWasAskedToGrantpasses. - Payments settled by restart recovery get no contact or notification (Bitkit/Services/PaykitAllowanceExecutor.swift:368). Fixed: Recovery now records through
recordintoapplyJournalOutcome(PaykitAllowanceExecutor.swift:508-531), which sends.paidAutomaticallywhen it moves an automatic entry to succeeded. The case reconciliation already settled is handled at :409-418.testRecoveryAnnouncesAPaymentReconciliationAlreadySettledpasses on my run, with one announcement across two recoveries. - Channel reconnection hides requests that require manual approval (Bitkit/Services/PaykitAllowanceManager.swift:290). Fixed:
processIncomingRequestshands over-limit requests back before the channel wait (PaykitAllowanceManager.swift:301-310).exceedsLimitsnow counts only allowances active atnow()(PaykitAllowanceExecutor.swift:439). For a 1,000-sat active allowance and a 2,000-sat request,fitsreturns false atPaykitAllowance.swift:299, so the request gets its manual id and line 1451 presents it.testLimitCheckIgnoresAnExpiredAllowancepasses. - Recovery and a settlement event can both announce one automatic payment (Bitkit/Services/PaykitAllowanceExecutor.swift:389). Fixed:
wasSettledandannounceAutomaticPaymentare gone. The only.paidAutomaticallysend is inapplyJournalOutcome(PaykitAllowanceExecutor.swift:525), whose check and stage write run in one synchronous closure on the actor. A settlement and recovery that both pass the SDK call can't both see the entry unsettled.testSettlementAndRecoveryAnnounceOnePaymentOncepasses. - Recovery and settlement can notify twice for one payment (Bitkit/Services/PaykitAllowanceExecutor.swift:389). Fixed: The same change answers this one: the decision to announce is made after the suspension inside
sdk.recordPaymentOutcome, by the synchronous journal update atPaykitAllowanceExecutor.swift:512-529. The interleaving you reproduced now yields one event, because the second caller finds the entry already succeeded.
new since the last review
worth doing, does not block
- Pay tapped during an allowance refresh spins until the next poll (Bitkit/AppScene.swift:1604). The new skip at
AppScene.swift:1604neither releasesrequestedPresentationIdnor schedules a retry, so a tapped Pay can wait up to the next 10-second poll. TheisAutomaticallyHandlingbranch above it does release. WhilerequestedPresentationIdis set,requestsForPresentationreturns only that request, andpresentNextItemskips subscription and allowance proposals. The Pay button keeps its spinner and every other Paykit sheet waits. Refreshes started by.ledgerChangedand.paidAutomatically(AppScene.swift:1952, :1969) have no presentation follow-up.finishManualPaymentemits.ledgerChanged, so paying one request from a contact and then tapping Pay on the next can hit this. Recovery then waits for the poll to reach line 1451.mayCovermatches ended, declined and expired allowances, none of which can start covering a request during a refresh. The skip also catches a request whose own Send sheet is already open and retrying, which line 1426 already keeps out of the pass. Found by reading, not reproduced. Two possible fixes: limitmayCoverto proposed or accepted, unexpired allowances; or callpresentNextIncomingPaykitItem()whenupdateDepthreturns to zero.
nits
- Recovery reads the Keychain journal once per settled attempt (Bitkit/Services/PaykitAllowanceExecutor.swift:412). The new
.succeeded, .failedbranch inrecovercallslocalState(identity:)for every terminal attempt in the SDK history (PaykitAllowanceExecutor.swift:412). Each call is a Keychain read plus a JSON decode of every identity's state. The loop has no await, so on a wallet with a long ledger history it holds the executor actor at launch, andisHandlingcalls from the presentation path queue behind it. Loading the journal once before the loop and reloading only afterapplyJournalOutcomewould avoid this.
A conversion only prices other rails, and the SDK requires a bitcoin rail to pay the bitcoin amount, so such a request is covered, as on Android. A manual payment made over the USDT rail stays out of the bitcoin ledger.
|
I pushed 98824b6, matching the Android twin synonymdev/bitkit-android#1340. A bitcoin-priced request that also prices a USDT rail through a conversion is now covered by an allowance and pays automatically over a bitcoin rail. The SDK's execution checks require the bitcoin amount on a bitcoin rail, so a payment can't be counted at a converted amount. A manual payment of such a request made over the USDT rail stays out of the bitcoin ledger. The allowance and payment request unit tests pass on this head. |
A request held back while allowances refresh is presented as soon as the update ends instead of at the next poll, only an allowance that could start covering the contact holds a request back, and recovery reads the journal once.
A list released when an automatic payment fails before broadcast is cleared from its journal entry first, so recording the attempt failed does not release it again after another payment consumed the same version.
|
@coreyphillips, thanks for the recheck. Your two non-blocking items and the finding from your #841 review are fixed in 4a1e391:
Fixed in 723cdf5:
Fixed in 723cdf5: recovery reads the journal once before the loop.
Fixed in 4a1e391. When an automatic payment fails before broadcast, its list version is cleared from the journal entry before the list is released, so recording the attempt as failed has nothing to release again. A durable release now happens only for an attempt whose list was never released: a settlement failure after an uncertain hand-off, or restart recovery. While that list version is still consumed, no other payment can consume it ( The allowance and private Paykit unit tests pass on this head. |
Recovery's cached journal stages only skip settled attempts; moving an attempt to failed checks its current stage in the same journal update, so a settlement that ran while recovery waited is not released twice. The allowance payability comment matches the conversion change.
|
I pushed d500fab for @coreyphillips's two notes on #799's code from his #841 approval:
The allowance unit tests pass on this head. |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 4 new commits (98824b6, 723cdf5, 4a1e391, d500fab). Nothing blocking remains.
addressed
- Pay tapped during an allowance refresh spins until the next poll (Bitkit/AppScene.swift:1604). Fixed:
mayCover(PaykitAllowanceManager.swift:343-348) now matches only proposed or currently active allowances.updateDepthis observed (:71), and AppScene.swift:716-721 re-runspresentNextIncomingPaykitItemwhenisUpdatingreturns to false. WithrequestedPresentationIdset, that goes straight to the request pass (AppScene.swift:208). The skip at :1610 is checked after theisHandlingawait, so a pass that resumes after the refresh ends proceeds, and one that skips is re-run on the transition.testManagerMayNotCoverAContactWhoseAllowanceEndedcovers the narrowing. - Recovery reads the Keychain journal once per settled attempt (Bitkit/Services/PaykitAllowanceExecutor.swift:412). Fixed:
recoverbuildsjournalStagesonce before the loop (PaykitAllowanceExecutor.swift:397-400). The.succeeded, .failedbranch looks up that dictionary at :419 and reaches the store throughapplyJournalOutcomeonly for an entry that was still open.
new since the last review
worth doing, does not block
- Settlement and recovery can each still release the same payment list (Bitkit/Services/PaykitAllowanceExecutor.swift:494). d500fab guards only recovery's
.succeeded, .failedbranch, so a failed attempt can still have its list released twice.record(PaykitAllowanceExecutor.swift:494-500) releases whenever the outcome is.failed. WithoutonlyIfOpen,applyJournalOutcomereturns the entry even when it is already.failed. The SDK does not stop the second report either: in paykit-rs v0.1.0history.rs,apply_outcomerejects only a change between terminal statuses, so reporting Failed twice succeeds. Two interleavings remain:1. lightningPaymentSettled filters the journal (:836) -> awaits sdk.recordPaymentOutcome recovery moves the attempt to failed and releases settlement resumes -> applyJournalOutcome returns the entry -> releases again 2. recovery .submitted/.unknown: verifiedOutcome reads the stage -> awaits the Lightning lookup a settlement records failed and releases recovery record(.failed) -> releases againThe second release undoes another payment's consumption only if that payment consumed the same version in between. That is the case raised on #841, and the author's reply says it can no longer happen. I confirmed this by reading the code and the SDK source; I did not reproduce it. One fix: inrecord, release only when this call moved the entry out of an open stage, assettledEntryalready does for Payment Executed (for example, passonlyIfOpen: truefor.failed). - A bitcoin-priced request that accepts only USDT is now held for an allowance (Bitkit/Services/PaykitAllowanceManager.swift:176). After 98824b6,
isAllowancePayableis justamount.asset == .btc(PaykitAllowance.swift:333-335), socoversRequestnow takes a bitcoin-priced request whose only accepted endpoint isusdt-arbitrum-address. Bitkit can send such a request: CreatePaymentRequestView lets the payee deselect Savings and Spending. An allowance can never pay it. The SDK'smatch_allowance_requestskips endpoints that carry a fixed conversion rate and returnsNoEligibleEndpoint. Bitkit still holds it, though: - WhilecanPayNow()is false,processIncomingRequestsholds it (PaykitAllowanceManager.swift:301-309). -isAutomaticallyHandlingkeeps it off the Send sheet (AppScene.swift:1600). - Tapping Pay shows the "already being paid" toast. So a request payable over USDT, which needs no channel, waits on the Lightning node, possibly indefinitely if channels stay unusable. Over the limits, it also raises Limit Reached throughexceedsLimitsor :606. Before this commit, the conversion check kept it manual. One fix: havecoversRequestrequire an allowed Lightning endpoint inrequest.acceptedPaymentEndpointIdentifiers. That would also stop on-chain-only requests from waiting on the channel. Found by reading the code and the SDK source; not run.
|
@coreyphillips, thanks for the approval. Your two non-blocking notes are tracked for 3.2.0 rather than pushed now, since a new head would make your approval stale:
Both are listed under Out of Scope. |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after replies, with no new commit since d500fab. Nothing blocking remains.
still open
- Settlement and recovery can each still release the same payment list (Bitkit/Services/PaykitAllowanceExecutor.swift:494). d500fab guards only recovery's
.succeeded, .failedbranch, so a failed attempt can still have its list released twice.record(PaykitAllowanceExecutor.swift:494-500) releases whenever the outcome is.failed. WithoutonlyIfOpen,applyJournalOutcomereturns the entry even when it is already.failed. The SDK does not stop the second report either: in paykit-rs v0.1.0history.rs,apply_outcomerejects only a change between terminal statuses, so reporting Failed twice succeeds. Two interleavings remain:1. lightningPaymentSettled filters the journal (:836) -> awaits sdk.recordPaymentOutcome recovery moves the attempt to failed and releases settlement resumes -> applyJournalOutcome returns the entry -> releases again 2. recovery .submitted/.unknown: verifiedOutcome reads the stage -> awaits the Lightning lookup a settlement records failed and releases recovery record(.failed) -> releases againThe second release undoes another payment's consumption only if that payment consumed the same version in between. That is the case raised on #841, and the author's reply says it can no longer happen. I confirmed this by reading the code and the SDK source; I did not reproduce it. One fix: inrecord, release only when this call moved the entry out of an open stage, assettledEntryalready does for Payment Executed (for example, passonlyIfOpen: truefor.failed). HEAD is still d500fab, andrecordat PaykitAllowanceExecutor.swift:494-500 still releases on any.failedoutcome withoutonlyIfOpen, so the double release stands. Your reply concedes the point and tracks it in #1089 (milestone 3.2.0, which describes both interleavings correctly). That is a valid deferral of a non-blocking point under the repo's policy, but it is not a fix or a rebuttal, so it stays open without holding the merge. - A bitcoin-priced request that accepts only USDT is now held for an allowance (Bitkit/Services/PaykitAllowanceManager.swift:176). After 98824b6,
isAllowancePayableis justamount.asset == .btc(PaykitAllowance.swift:333-335), socoversRequestnow takes a bitcoin-priced request whose only accepted endpoint isusdt-arbitrum-address. Bitkit can send such a request: CreatePaymentRequestView lets the payee deselect Savings and Spending. An allowance can never pay it. The SDK'smatch_allowance_requestskips endpoints that carry a fixed conversion rate and returnsNoEligibleEndpoint. Bitkit still holds it, though: - WhilecanPayNow()is false,processIncomingRequestsholds it (PaykitAllowanceManager.swift:301-309). -isAutomaticallyHandlingkeeps it off the Send sheet (AppScene.swift:1600). - Tapping Pay shows the "already being paid" toast. So a request payable over USDT, which needs no channel, waits on the Lightning node, possibly indefinitely if channels stay unusable. Over the limits, it also raises Limit Reached throughexceedsLimitsor :606. Before this commit, the conversion check kept it manual. One fix: havecoversRequestrequire an allowed Lightning endpoint inrequest.acceptedPaymentEndpointIdentifiers. That would also stop on-chain-only requests from waiting on the channel. Found by reading the code and the SDK source; not run.isAllowancePayableis still justamount.asset == .btc(PaykitAllowance.swift:333-335), andcoversRequest(PaykitAllowanceManager.swift:176) still ignoresacceptedPaymentEndpointIdentifiers, so a USDT-only bitcoin request is still held. Your reply agrees and tracks it in #1090 (milestone 3.2.0, accurate description and suggested fix). As a deferral rather than a code change or a rebuttal, it stays open, non-blocking.
Closes #806
Twin: synonymdev/bitkit-android#1340
Refs:
This PR adds allowances: a payer sets a per-payment and a monthly limit for a Paykit contact, and that contact's requests within the limits are paid without asking.
Description
It builds on the Paykit SDK
0.1.0that master pins, where an Allowance binds the two Pubky identities of a contact's single Encrypted Link and covers every Paykit app they use.Out of Scope
Design
Figma "Bitkit - Experimental New": Allowances empty state, Set Allowance, Allowances list. The receiver's review sheet and the detail sheet have no frame and follow the drawn film. Deliberate differences: the first tab stays "Overview", the list row's right column reads the monthly limit, and the copy typos are fixed.
Preview
auto-pay-clip.mp4
QA Notes
Journeys
At b005f8f the journeys ran cross-device against the Android twin synonymdev/bitkit-android#1340, on an iPhone 17 Pro simulator (iOS 26.5) and an Android 15 emulator on staging regtest; results and recordings are in the QA comment.
set-and-accept.xml— the offer opens on the payee by itself and both rows turn Active; passed with iOS as payee and as payerauto-pay-under-limit.xml— a $2 request pays itself, the Payments row reads Auto-paid and the detail shows $2.00 paid automatically; passed with iOS as payee and as payerabove-limit-asks.xml— a $20 request raises Limit Reached and arrives as an ordinary Payment Request, paid only by swipe and not tagged Auto-paid; passed with iOS as payeemonthly-cap-reached.xml— the third $4 request on a $10 cap raises Limit Reached and stays an ordinary Payment Request, and the detail reads $8.00 paid automatically; passed with iOS as payeeend-stops-auto-pay.xml— after either side ends it, both rows read Ended and the next request asks; both halves passed with iOS as payee, and the payer-ends half with iOS as payerpin-gates-allowance-changes.xml(ported from the Android twin) — the iOS payer's Save and End ask for the payment PIN, Back leaves the allowance unchanged, and the payee's Accept never asks; passed on two iOS simulators (QA), and with Android as payer in the cross-device runrestart-never-pays-twice.xml— the payer was killed 0.4 s after the hand-off and relaunched: one payment left the wallet and the payee received one; ran on two iOS 26.5 simulators before the master sync, against Paykit0.1.0-rc59Manual Tests
N/A
Automated Checks
PaykitAllowanceTests.swift— limits, terms, capacity, month anchoring, grouping and admission decisionsPaykitAllowanceExecutorTests.swift— reserve, hand-off, claim, payment list outcomes, cancellation of the caller, outcome recording, deferral, channel readiness, granting over a ready link and restart recovery of automatic payments, Lightning-only automatic payment, uncertain Lightning submission errors, a settlement during the send, uncapped terms, the routing-fee cap, an unknown request creation time, an unsaved sending stage, the per-payment limit notification, requests over the limits while a channel reconnects, an execution startup error, an acceptance intent the manual flow saved, and announcing payments recovery settlesPaykitPaymentProofServiceTests.swift— payment proofs carry the allowance idPaykitPaymentStateBackupTests.swift— an automatic payment proof keeps its allowance id through backup, and older backups still restoreBitkitTestson an iOS 26.5 simulator against Paykit0.1.0after the master sync — all pass exceptSendConfirmationLifecycleTests.testCustomFeeWalletSwitchSurvivesFeeScreenNavigation, which fails the same way on master