Skip to content

feat: accept allowance offers automatically on the allowee's wallet - #1385

Merged
ovitrif merged 31 commits into
masterfrom
feat/allowance-auto-accept
Oct 22, 2026
Merged

ovitrif merged 31 commits into
masterfrom
feat/allowance-auto-accept

Conversation

@ovitrif

@ovitrif ovitrif commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1353
Twin: synonymdev/bitkit-ios#841
Refs: #1351

This PR makes the allowee's wallet accept an allowance the allower offers, so the allowee taps nothing and the allowance is active as soon as the offer arrives.

Description

  • Adds automatic acceptance of a received allowance proposal that makes this wallet the allowee, so the allower's decision alone activates the allowance.
  • Adds an "Allowance Added" notification, "{name} set up an allowance for you", with a toast fallback, so the allowee learns about the new allowance.
  • Keeps the review sheet with Accept and Decline for an allowee's ask, because there the money is this wallet's.
  • Retries a failed acceptance on the next refresh, and never touches a proposal this wallet sent or one already answered.
  • Accepts only the offers it checked. When the app leaves the foreground between polls it stops accepting further offers but still announces the ones it accepted, and it announces nothing once the profile changed or is being deleted. An accept cut off by the polling job's cancellation is not announced yet (bug: allowance added notice is lost when the app leaves during an automatic accept #1672).

Out of Scope

Design

N/A — no design available.

Preview

Payer: Set Allowance Payer: waiting for an answer Payer: Active
Allowee: Allowance Added notification Allowee: notification shade Allowee: row Active

ba1353-set-and-accept-1b61baa-2x.mp4

QA Notes

Journeys

  • updated set-and-accept.xml — the allowee's wallet accepts the offer with no review sheet, posts "Allowance Added", and both rows turn Active
    • Driven again on two fresh staging wallets at 1b61baa, Paykit 0.1.0, recorded side by side above (payer left, allowee right, 2x): no review sheet opened, "Allowance Added" posted once, the payer's row read "Waiting for an answer" and then "Active · $5 a payment", and the allowee's row read "Active · $5 a payment". The proposal went out 11 s after Save and the allowee's wallet accepted it and posted "Allowance Added" 17 s later (28 s from Save). The later head ba1c9bc only adds base changes outside the offer accept.
    • Journey drift, not edited here: Choose Contact shows "Add a contact first" for about 3 minutes after the contacts are saved, until Paykit discovery finishes (about 2.5 min at 1b61baa).
  • updated pin-gates-allowance-changes.xml — with a payment PIN on the payer, the payee's wallet still accepts the offer by itself: "Allowance Added" arrives with no review sheet, no tap and no PIN
    • Not driven on a device in this PR; the PIN gate itself comes from the base.

Manual Tests

N/A

Automated Checks

  • added PaykitAllowanceTest.kt — only a received proposal from the allower counts as an offer
  • added PaykitAllowanceRepoTest.kt — an allower's offer is accepted without the sheet and not announced twice; a failed accept is retried and announced once; an allowee's ask still goes to the sheet; sent and answered proposals are never accepted; an ask that joins an offer's entry mid-accept is never accepted; an offer accepted as the app backgrounds is still announced and no further offer is accepted; nothing is announced once the identity changed or the profile is being deleted
  • updated AppViewModelSendFlowTest.kt — the send flow stubs the new acceptance call
  • ran ./gradlew testDevDebugUnitTest detekt at 8113f8e (Paykit 0.1.0, merged with the updated feat/paykit-allowances): unit tests pass and detekt is clean

@ovitrif ovitrif self-assigned this Sep 30, 2026
@ovitrif

ovitrif commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 528fb7b: merged the updated feat/paykit-allowances (Paykit rc59, allowances bound to the contact identity) into this branch.

  • The auto-accept code needed no change, since it already addresses an allowance by counterparty and allowance id.
  • The new tests now use the identity-bound acceptAllowance(counterparty, allowanceId) and processOutboundPrivateMessages(counterparty) calls (a3861ce).
  • The rule is unchanged: only a received proposal that makes this wallet the allowee is accepted automatically; an ask from the allowee still opens the review sheet.
  • testDevDebugUnitTest (3,220 tests) and detekt pass.
  • Device run of set-and-accept.xml on two emulators is still pending; screenshots and a recording follow in the description.

@ovitrif

ovitrif commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Added the device result and media to the description.

  • set-and-accept.xml passes on two fresh staging wallets with the a3861ce build (Paykit rc59): the allowee accepted with no tap and no review sheet, "Allowance Added" posted once, and both rows turned Active.
  • Save to the allowee's row took about 3 min 16 s on a heavily loaded host.
  • The build was a3861ce; head 528fb7b only adds a base merge that touches PaykitAllowanceRepo.kt (one line) and PaykitPaymentRequestRepo.kt.
  • Preview now has the payer and allowee screenshots plus the recording.
  • Journey drift is noted under QA Notes and the journey is not edited here.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed e7a8bd3: merged the updated feat/paykit-allowances (b929cce, Ben's newer shared-runtime sync and Paykit session retry) into this branch.

  • The merge was clean and changes none of this PR's code.
  • testDevDebugUnitTest (3,227 tests) and detekt pass on the merged head.
  • The device run was on the a3861ce build, before this merge, and has not been repeated.

@jvsena42
jvsena42 added this pull request to stack #1415 October 2, 2026 11:50
@ovitrif
ovitrif removed this pull request from stack #1415 October 5, 2026 15:39
@ovitrif

ovitrif commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed df5105a: merged the updated feat/paykit-allowances (now synced with master, Paykit 0.1.0) into this branch.

  • AppViewModel.kt: the base now runs allowance payments inside a payment submission (processAllowanceRequests()). The offer accept stays just before it, outside that gate, because accepting an allowance sends no payment.
  • PaykitAllowanceRepoTest.kt: Paykit 0.1.0's acceptAllowance takes the proposal's stream item id. The tests now stub and verify the three-argument call. Auto-accept passes the id through the existing accept path, so the app code needed no change.
  • The rule is unchanged: only a received proposal that makes this wallet the allowee is accepted automatically; an ask from the allowee still opens the review sheet.
  • testDevDebugUnitTest and detekt pass on the merged head.
  • A device rerun of set-and-accept.xml on this head follows, with the time from proposal to the payee's Active row in QA Notes.

@ovitrif ovitrif added this to the 3.1.0 milestone Oct 19, 2026
…t/allowance-auto-accept

# Conflicts:
#	journeys/allowances/README.md
…t/allowance-auto-accept

# Conflicts:
#	app/src/test/java/to/bitkit/repositories/PaykitAllowanceRepoTest.kt
@ovitrif

ovitrif commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 211d4c2: merged the updated feat/paykit-allowances (PIN gate for the allower, bounded-limits fix, master sync) into this branch.

  • pin-gates-allowance-changes.xml: the new journey expected the payee to open the review sheet and tap Accept. On this branch the payee's wallet accepts the offer by itself, so the journey now checks that "Allowance Added" arrives with no review sheet, no tap and no PIN, and that a cancelled PIN on the payer sends nothing.
  • journeys/allowances/README.md: kept the base's PIN paragraph and this branch's auto-accept paragraph.
  • PaykitAllowanceRepoTest.kt: kept the auto-accept tests next to the base's new test for a proposal without limits.
  • The PIN gate covers only the allower's own Save, End and Accept; the allowee's automatic accept goes through the repository and never asks, as the journey README states.
  • testDevDebugUnitTest and detekt pass on the merged head.
  • set-and-accept.xml passed on df5105a (proposal to the payee's Active row in about 20 s, recording in the description); a rerun on this head follows.

@ovitrif

ovitrif commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 1b61baa: merged the updated feat/paykit-allowances into this branch.

  • The base now pays allowance requests automatically only over Lightning; on-chain requests stay manual. The automatic accept sends no payment, so it is unchanged.
  • The allowee's row now shows the per-payment limit in whole dollars like the payer's ("Active · $5 a payment"), from the base.
  • No conflicts; testDevDebugUnitTest and detekt pass on the merged head.
  • The set-and-accept.xml rerun on this head follows.

@ovitrif

ovitrif commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed ba1c9bc: merged the updated feat/paykit-allowances (master sync, a routing-fee cap for automatic payments, and dropping allowance terms whose limits cannot be read) into this branch.

  • No conflicts. The new allowance fixes apply only when this wallet pays, and the master changes (USDT screens, timed sheets, send amounts) leave the offer accept in AppViewModel.kt as it was.
  • testDevDebugUnitTest and detekt pass on the merged head.
  • set-and-accept.xml passed on 1b61baa with both rows reading "Active · $5 a payment" (recording and timing in the description). This merge adds nothing on that path, so it was not driven again.

@ovitrif
ovitrif marked this pull request as ready for review October 19, 2026 20:10
@ovitrif
ovitrif requested review from a team and ben-kaufman October 19, 2026 20:10
@ovitrif
ovitrif changed the base branch from feat/paykit-allowances to master October 21, 2026 18:41
@ovitrif
ovitrif dismissed pwltr’s stale review October 21, 2026 18:41

The base branch was changed.

@ovitrif

ovitrif commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Retargeted onto master ahead of merging #1340, so this PR stays open when its base branch is deleted. It merges after #1340; until then the diff also shows that PR's commits.

Comment thread app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt
@ovitrif

ovitrif commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed f93e1bc:

  • PaykitAllowanceRepo.kt, AppViewModel.kt: an offer accepted as the app leaves the foreground is still announced; only further accepts stop. An identity change or a profile deletion still announces nothing.
  • values-*/strings.xml: the two new strings ("Allowance Added" and "{name} set up an allowance for you") are translated into every supported language, as the repository now requires.
  • testDevDebugUnitTest, detekt, lintDevDebug and assembleDevDebug pass.

@ovitrif
ovitrif force-pushed the feat/allowance-auto-accept branch from f93e1bc to cda8521 Compare October 22, 2026 11:55
@ovitrif

ovitrif commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Force-pushed cda8521: rewrote this branch so every commit in it is signed and verified, which master requires. It is now the single signed commit that #1340 became, plus one signed commit for this feature. The tree is identical to the previous head f93e1bc, so the code under review is unchanged; the history no longer carries the two unsigned sync commits from 2 Oct.

@ben-kaufman @coreyphillips could you review? The approval this PR had was dismissed only because its base changed to master.

@ovitrif
ovitrif requested a review from coreyphillips October 22, 2026 11:55

@coreyphillips coreyphillips left a comment

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.

Two independent reviews, nothing blocking a merge.

worth doing, does not block

  • Offer accepted as the polling job is cancelled is never announced (app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt:306). An offer whose SDK accept completes while backgrounding cancels the polling job becomes active but is never announced. On ON_STOP, ContentView.kt:323-324 calls stopPaykitPaymentRequestPolling(). That cancels paykitPaymentRequestPollingJob (AppViewModel.kt:1458), the coroutine running the 10 s refreshIncomingPaykitPaymentRequests and so acceptOffersFromAllowers. PaykitSdkService.completeSdkCall (PaykitSdkService.kt:1687-1693) runs acceptAllowance under NonCancellable and then calls ensureActive(). So the SDK records the acceptance and a CancellationException is thrown. runSuspendCatching in respond and sendQueuedMessages rethrows it, accepted += offer never runs, and announceAcceptedAllowanceOffers is skipped. On the next refresh the record is ACCEPTED, so it is not answerable and is never announced. This contradicts the PR's claim that it "still announces what it accepted". The canContinue flag only helps callers outside the polling job, and offer accepted as the app backgrounds is still announced... tests the flag, not cancellation. Process death right after the accept ends the same way. Impact is small: the allowance is active and listed, and only the "Allowance Added" notice is lost. Confirmed by reading the code paths above; I could not run the unit tests locally (no GitHub Packages access for Paykit). Possible fixes: persist accepted-but-unannounced keys in PaykitAllowanceLocalState and announce from there, or run each accept and its bookkeeping in withContext(NonCancellable).
  • Backgrounding can permanently suppress the allowance notification (app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt:306). An offer accepted while the app backgrounds can become active without notifying the allowee. stopPaykitPaymentRequestPolling() cancels the poll, while completeSdkCall() lets acceptance commit before rethrowing cancellation. The announcement is then skipped. Later refreshes exclude the accepted offer, so the notification is permanently lost. A focused reproduction using the copied acceptance methods confirmed one committed acceptance and zero announcements after refresh.
  • Journal pruning erases allowance totals and payment history labels (app/src/main/java/to/bitkit/repositories/PaykitAllowanceExecutor.kt:840). Allowance totals decrease when settled payments leave the 200-entry journal. PaykitAllowanceRepo.publish() derives both “Paid so far” and the “Auto-paid” labels solely from this journal, which also holds manual attempts. One automatic payment followed by 200 manual request payments therefore erases that allowance's displayed total and the older payment's label. A focused reproduction using the copied journal function confirmed the total dropping from 200 sats to zero.

nits

  • Allowance journeys still describe a payee accept step (journeys/allowances/monthly-cap-reached.xml:11). monthly-cap-reached.xml step 2 still says "and have the payee accept it". The payee no longer does anything, so a runner may look for a review sheet that never opens. In pin-gates-allowance-changes.xml, the step starting "On the payee instance" ends with "and the payer's row turns Active", which mixes the two instances. Suggest "wait for the payee's wallet to accept it" and naming which instance's row is checked.
  • Test for an ask joining an offer's entry cannot fail (app/src/test/java/to/bitkit/repositories/PaykitAllowanceRepoTest.kt). offer accept never answers an ask that joined the entry after the check adds the ask under a different counterparty (asker). Entries are keyed by counterparty and allowance id, so the ask lands in its own entry and the assertion holds even with the old lookup by entry id. Groups only ever hold the single allowance id this wallet proposed (PaykitAllowanceRepo.kt:266), so a received ask cannot join an offer's entry. The scenario in the test name and in the comment at PaykitAllowanceRepo.kt:305 is unreachable. Consider dropping or renaming the test and the comment.

@ovitrif

ovitrif commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

@coreyphillips thanks. With the 3.1.0 freeze, the non-blocking points go to 3.2.0 rather than this PR:

@coreyphillips coreyphillips left a comment

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.

Rechecked after replies, with no new commit since cda8521. Nothing blocking remains.

still open

  • Offer accepted as the polling job is cancelled is never announced (app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt:306). An offer whose SDK accept completes while backgrounding cancels the polling job becomes active but is never announced. On ON_STOP, ContentView.kt:323-324 calls stopPaykitPaymentRequestPolling(). That cancels paykitPaymentRequestPollingJob (AppViewModel.kt:1458), the coroutine running the 10 s refreshIncomingPaykitPaymentRequests and so acceptOffersFromAllowers. PaykitSdkService.completeSdkCall (PaykitSdkService.kt:1687-1693) runs acceptAllowance under NonCancellable and then calls ensureActive(). So the SDK records the acceptance and a CancellationException is thrown. runSuspendCatching in respond and sendQueuedMessages rethrows it, accepted += offer never runs, and announceAcceptedAllowanceOffers is skipped. On the next refresh the record is ACCEPTED, so it is not answerable and is never announced. This contradicts the PR's claim that it "still announces what it accepted". The canContinue flag only helps callers outside the polling job, and offer accepted as the app backgrounds is still announced... tests the flag, not cancellation. Process death right after the accept ends the same way. Impact is small: the allowance is active and listed, and only the "Allowance Added" notice is lost. Confirmed by reading the code paths above; I could not run the unit tests locally (no GitHub Packages access for Paykit). Possible fixes: persist accepted-but-unannounced keys in PaykitAllowanceLocalState and announce from there, or run each accept and its bookkeeping in withContext(NonCancellable). acceptOffersFromAllowers at PaykitAllowanceRepo.kt:301-310 is unchanged, so cancellation after a committed accept still skips accepted += offer and the announcement. The deferral to #1672 is real: it is open, milestoned 3.2.0, and describes this exact path. It does not show the point wrong, so the point stays open as a tracked non-blocking item. The Description bullet "still announces what it accepted" is still only accurate outside the cancellation path.
  • Allowance journeys still describe a payee accept step (journeys/allowances/monthly-cap-reached.xml:11). monthly-cap-reached.xml step 2 still says "and have the payee accept it". The payee no longer does anything, so a runner may look for a review sheet that never opens. In pin-gates-allowance-changes.xml, the step starting "On the payee instance" ends with "and the payer's row turns Active", which mixes the two instances. Suggest "wait for the payee's wallet to accept it" and naming which instance's row is checked. Monthly-cap-reached.xml:11 still says "have the payee accept it". The payee step in pin-gates-allowance-changes.xml still ends with the payer's row turning Active. Both are listed as follow-ups in #1672, but the files are unchanged.
  • Test for an ask joining an offer's entry cannot fail (app/src/test/java/to/bitkit/repositories/PaykitAllowanceRepoTest.kt). offer accept never answers an ask that joined the entry after the check adds the ask under a different counterparty (asker). Entries are keyed by counterparty and allowance id, so the ask lands in its own entry and the assertion holds even with the old lookup by entry id. Groups only ever hold the single allowance id this wallet proposed (PaykitAllowanceRepo.kt:266), so a received ask cannot join an offer's entry. The scenario in the test name and in the comment at PaykitAllowanceRepo.kt:305 is unreachable. Consider dropping or renaming the test and the comment. The test at PaykitAllowanceRepoTest.kt:425 still adds the ask under a different counterparty (asker), so it cannot fail. The comment at PaykitAllowanceRepo.kt:305 still describes an unreachable case. Tracked in #1672, unchanged here.
  • Backgrounding can permanently suppress the allowance notification (app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt:306). An offer accepted while the app backgrounds can become active without notifying the allowee. stopPaykitPaymentRequestPolling() cancels the poll, while completeSdkCall() lets acceptance commit before rethrowing cancellation. The announcement is then skipped. Later refreshes exclude the accepted offer, so the notification is permanently lost. A focused reproduction using the copied acceptance methods confirmed one committed acceptance and zero announcements after refresh. This is the same defect as point 1, and the code at PaykitAllowanceRepo.kt:306 and AppViewModel.kt:1192-1196 is unchanged. Deferring it to #1672 tracks it but does not answer it.
  • Journal pruning erases allowance totals and payment history labels (app/src/main/java/to/bitkit/repositories/PaykitAllowanceExecutor.kt:840). Allowance totals decrease when settled payments leave the 200-entry journal. PaykitAllowanceRepo.publish() derives both “Paid so far” and the “Auto-paid” labels solely from this journal, which also holds manual attempts. One automatic payment followed by 200 manual request payments therefore erases that allowance's displayed total and the older payment's label. A focused reproduction using the copied journal function confirmed the total dropping from 200 sats to zero. Codex still sees it: PaykitAllowanceExecutor.kt:840-841 still prunes the shared journal to 200 entries, while PaykitAllowanceRepo.kt:559-572 derives totals and labels from retained entries. The response references #1584 without fixing or rebutting the defect.

@ovitrif
ovitrif force-pushed the feat/allowance-auto-accept branch from cda8521 to e45f4ec Compare October 22, 2026 12:33
@ovitrif

ovitrif commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Force-pushed e45f4ec: re-signed the whole branch history instead of the earlier two-commit rewrite (cda8521), so every commit is signed and verified while keeping its tree, message, author and dates. The commits shared with #1340 are exactly its re-signed ones. The head's tree is identical to f93e1bc and cda8521, so the code under review is unchanged.

@ovitrif
ovitrif requested review from coreyphillips and removed request for coreyphillips October 22, 2026 12:33
coreyphillips
coreyphillips previously approved these changes Oct 22, 2026

@coreyphillips coreyphillips left a comment

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.

Rechecked after the branch was rewritten, so every earlier point was verified again from scratch. Nothing blocking remains.

still open

  • Offer accepted as the polling job is cancelled is never announced (app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt:306). An offer whose SDK accept completes while backgrounding cancels the polling job becomes active but is never announced. On ON_STOP, ContentView.kt:323-324 calls stopPaykitPaymentRequestPolling(). That cancels paykitPaymentRequestPollingJob (AppViewModel.kt:1458), the coroutine running the 10 s refreshIncomingPaykitPaymentRequests and so acceptOffersFromAllowers. PaykitSdkService.completeSdkCall (PaykitSdkService.kt:1687-1693) runs acceptAllowance under NonCancellable and then calls ensureActive(). So the SDK records the acceptance and a CancellationException is thrown. runSuspendCatching in respond and sendQueuedMessages rethrows it, accepted += offer never runs, and announceAcceptedAllowanceOffers is skipped. On the next refresh the record is ACCEPTED, so it is not answerable and is never announced. This contradicts the PR's claim that it "still announces what it accepted". The canContinue flag only helps callers outside the polling job, and offer accepted as the app backgrounds is still announced... tests the flag, not cancellation. Process death right after the accept ends the same way. Impact is small: the allowance is active and listed, and only the "Allowance Added" notice is lost. Confirmed by reading the code paths above; I could not run the unit tests locally (no GitHub Packages access for Paykit). Possible fixes: persist accepted-but-unannounced keys in PaykitAllowanceLocalState and announce from there, or run each accept and its bookkeeping in withContext(NonCancellable). The code is unchanged: HEAD's tree is identical to cda8521's. respond(offer) at PaykitAllowanceRepo.kt:306 still runs the bookkeeping outside NonCancellable. completeSdkCall (PaykitSdkService.kt:1687-1693) still calls ensureActive() after the SDK commits, and stopPaykitPaymentRequestPolling (AppViewModel.kt:1454-1459) still cancels the polling job. The PR description now states the limitation accurately and tracks it in #1672 (milestone 3.2.0). That is a reasonable deferral for a non-blocking point, but the code does not answer it yet.
  • Allowance journeys still describe a payee accept step (journeys/allowances/monthly-cap-reached.xml:11). monthly-cap-reached.xml step 2 still says "and have the payee accept it". The payee no longer does anything, so a runner may look for a review sheet that never opens. In pin-gates-allowance-changes.xml, the step starting "On the payee instance" ends with "and the payer's row turns Active", which mixes the two instances. Suggest "wait for the payee's wallet to accept it" and naming which instance's row is checked. Monthly-cap-reached.xml:11 still ends with "and have the payee accept it". The pin-gates-allowance-changes.xml step "On the payee instance, verify 'Allowance Added' arrives..." still ends by checking "the payer's row turns Active". Both are listed in #1672 for later; the journeys themselves are unchanged.
  • Test for an ask joining an offer's entry cannot fail (app/src/test/java/to/bitkit/repositories/PaykitAllowanceRepoTest.kt). offer accept never answers an ask that joined the entry after the check adds the ask under a different counterparty (asker). Entries are keyed by counterparty and allowance id, so the ask lands in its own entry and the assertion holds even with the old lookup by entry id. Groups only ever hold the single allowance id this wallet proposed (PaykitAllowanceRepo.kt:266), so a received ask cannot join an offer's entry. The scenario in the test name and in the comment at PaykitAllowanceRepo.kt:305 is unreachable. Consider dropping or renaming the test and the comment. offer accept never answers an ask that joined the entry after the check (PaykitAllowanceRepoTest.kt:425) still adds the ask under asker, a different counterparty, so it lands in its own entry and the assertion cannot fail. The comment at PaykitAllowanceRepo.kt:305 is also unchanged. The rename and comment removal are deferred to #1672.
  • Backgrounding can permanently suppress the allowance notification (app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt:306). An offer accepted while the app backgrounds can become active without notifying the allowee. stopPaykitPaymentRequestPolling() cancels the poll, while completeSdkCall() lets acceptance commit before rethrowing cancellation. The announcement is then skipped. Later refreshes exclude the accepted offer, so the notification is permanently lost. A focused reproduction using the copied acceptance methods confirmed one committed acceptance and zero announcements after refresh. This is the same path as point 1 and is unchanged at PaykitAllowanceRepo.kt:303-310. An accept that commits while the polling job is cancelled still skips accepted += offer and the announcement. Later refreshes filter it out with isAnswerable, so the notice is never shown. It is tracked in #1672, not fixed.
  • Journal pruning erases allowance totals and payment history labels (app/src/main/java/to/bitkit/repositories/PaykitAllowanceExecutor.kt:840). Allowance totals decrease when settled payments leave the 200-entry journal. PaykitAllowanceRepo.publish() derives both “Paid so far” and the “Auto-paid” labels solely from this journal, which also holds manual attempts. One automatic payment followed by 200 manual request payments therefore erases that allowance's displayed total and the older payment's label. A focused reproduction using the copied journal function confirmed the total dropping from 200 sats to zero. journal() at PaykitAllowanceExecutor.kt:837-841 still keeps only takeLast(MAX_JOURNAL_ENTRIES) (200, line 151). Manual attempts still share that list. publish() (PaykitAllowanceRepo.kt:559-575) still derives "Paid so far" and the "Auto-paid" ids only from it. This comes from the stacked base and is tracked in #1584 (milestone 3.2.0); the code is unchanged.

@ovitrif
ovitrif dismissed coreyphillips’s stale review October 22, 2026 13:02

The merge-base changed after approval.

@ovitrif

ovitrif commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 8113f8e: merged master now that #1340 has landed, so this PR's diff is only the auto-accept feature. No conflicts; testDevDebugUnitTest, detekt, lintDevDebug and assembleDevDebug pass.

@ovitrif
ovitrif requested a review from coreyphillips October 22, 2026 13:07
@ovitrif

ovitrif commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

@coreyphillips the master merge after #1340 landed dismissed your approval; the feature code is unchanged since your recheck at e45f4ec. Could you re-approve?

@coreyphillips coreyphillips left a comment

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.

Rechecked after 17 new commits (0d06387, 493b7de, 85ff0d1, and 14 more). Nothing blocking remains.

still open

  • Offer accepted as the polling job is cancelled is never announced (app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt:306). An offer whose SDK accept completes while backgrounding cancels the polling job becomes active but is never announced. On ON_STOP, ContentView.kt:323-324 calls stopPaykitPaymentRequestPolling(). That cancels paykitPaymentRequestPollingJob (AppViewModel.kt:1458), the coroutine running the 10 s refreshIncomingPaykitPaymentRequests and so acceptOffersFromAllowers. PaykitSdkService.completeSdkCall (PaykitSdkService.kt:1687-1693) runs acceptAllowance under NonCancellable and then calls ensureActive(). So the SDK records the acceptance and a CancellationException is thrown. runSuspendCatching in respond and sendQueuedMessages rethrows it, accepted += offer never runs, and announceAcceptedAllowanceOffers is skipped. On the next refresh the record is ACCEPTED, so it is not answerable and is never announced. This contradicts the PR's claim that it "still announces what it accepted". The canContinue flag only helps callers outside the polling job, and offer accepted as the app backgrounds is still announced... tests the flag, not cancellation. Process death right after the accept ends the same way. Impact is small: the allowance is active and listed, and only the "Allowance Added" notice is lost. Confirmed by reading the code paths above; I could not run the unit tests locally (no GitHub Packages access for Paykit). Possible fixes: persist accepted-but-unannounced keys in PaykitAllowanceLocalState and announce from there, or run each accept and its bookkeeping in withContext(NonCancellable). The code is unchanged: completeSdkCall (PaykitSdkService.kt:1687-1692) still calls ensureActive() after the NonCancellable accept, and stopPaykitPaymentRequestPolling (AppViewModel.kt:1459) still cancels the job, so accepted += offer at PaykitAllowanceRepo.kt:306 is skipped. The description now admits the gap and #1672 (3.2.0) tracks it, so it need not hold the merge, but a deferral does not close it.
  • Allowance journeys still describe a payee accept step (journeys/allowances/monthly-cap-reached.xml:11). monthly-cap-reached.xml step 2 still says "and have the payee accept it". The payee no longer does anything, so a runner may look for a review sheet that never opens. In pin-gates-allowance-changes.xml, the step starting "On the payee instance" ends with "and the payer's row turns Active", which mixes the two instances. Suggest "wait for the payee's wallet to accept it" and naming which instance's row is checked. Monthly-cap-reached.xml:11 still ends with "and have the payee accept it". In pin-gates-allowance-changes.xml, the "On the payee instance" step still ends with "and the payer's row turns Active". The description defers both to #1672, but the wording has not changed.
  • Test for an ask joining an offer's entry cannot fail (app/src/test/java/to/bitkit/repositories/PaykitAllowanceRepoTest.kt). offer accept never answers an ask that joined the entry after the check adds the ask under a different counterparty (asker). Entries are keyed by counterparty and allowance id, so the ask lands in its own entry and the assertion holds even with the old lookup by entry id. Groups only ever hold the single allowance id this wallet proposed (PaykitAllowanceRepo.kt:266), so a received ask cannot join an offer's entry. The scenario in the test name and in the comment at PaykitAllowanceRepo.kt:305 is unreachable. Consider dropping or renaming the test and the comment. offer accept never answers an ask that joined the entry after the check (PaykitAllowanceRepoTest.kt:425) is unchanged, and so is the comment at PaykitAllowanceRepo.kt:305. Both still describe a case that cannot happen. The description defers this to #1672.
  • Backgrounding can permanently suppress the allowance notification (app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt:306). An offer accepted while the app backgrounds can become active without notifying the allowee. stopPaykitPaymentRequestPolling() cancels the poll, while completeSdkCall() lets acceptance commit before rethrowing cancellation. The announcement is then skipped. Later refreshes exclude the accepted offer, so the notification is permanently lost. A focused reproduction using the copied acceptance methods confirmed one committed acceptance and zero announcements after refresh. This is the same path as point 1, and nothing in PaykitAllowanceRepo.kt, AppViewModel.kt or PaykitSdkService.kt changed since e45f4ec (I checked git diff e45f4ec..HEAD on those files). An accept that the poll cancellation cuts off is still never announced. It is tracked in #1672.
  • Journal pruning erases allowance totals and payment history labels (app/src/main/java/to/bitkit/repositories/PaykitAllowanceExecutor.kt:840). Allowance totals decrease when settled payments leave the 200-entry journal. PaykitAllowanceRepo.publish() derives both “Paid so far” and the “Auto-paid” labels solely from this journal, which also holds manual attempts. One automatic payment followed by 200 manual request payments therefore erases that allowance's displayed total and the older payment's label. A focused reproduction using the copied journal function confirmed the total dropping from 200 sats to zero. Codex still sees it: PaykitAllowanceExecutor.kt:840 still prunes the shared journal to 200 entries. PaykitAllowanceRepo.kt:559 still derives totals and payment labels solely from its remaining successful automatic entries.

@ovitrif

ovitrif commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

QA:
Smoke test of this PR at 8113f8e (Android, allowee) against iOS master at ec2d68a (payer, includes synonymdev/bitkit-ios#799 and synonymdev/bitkit-ios#841), on an Android 15 emulator and an iOS 26.5 simulator against staging regtest. Both wallets were fresh, with Pubky profiles linked as contacts; the payer had a 100,000-sat spending balance and the allowee 150,000 sats of inbound capacity through staging LSP channels.

🟢 Tests 1, 2
Test 1

The payer sets a $5 a payment, $50 a month allowance; the allowee's wallet accepts it by itself. "Allowance Added" ("AlwPayer set up an allowance for you") posted on Android 24 s after Save, with no review sheet and no tap. The payer's row read "Active · $5 a payment" 41 s after Save; the allowee's row reads the same.

iOS payer · Android allowee (2x)
c1-sbs-2x.mp4

Test 2

The allowee sends a one-time $2 Payment Request ("Devlog"); the payer pays it automatically over Lightning. The payer's spending dropped by 2,454 sats (2,452 plus a 2-sat fee) within 56 s of the request, its Activity lists "Sent to AlwPayee $2.00", and the allowee showed "Received Instant Bitcoin ₿2,452" 72 s after sending.

iOS payer · Android allowee (2x)
c2-sbs-2x.mp4

Note

The recordings are choppy (7 to 20 fps of motion) because the shared test host was under load (load 16 to 43 on 10 cores) during the run; the results above are read from the devices' state and logs.

@ovitrif
ovitrif merged commit 801fea0 into master Oct 22, 2026
21 checks passed
@ovitrif
ovitrif deleted the feat/allowance-auto-accept branch October 22, 2026 13:55
@ovitrif ovitrif mentioned this pull request Oct 27, 2026
4 tasks
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.

feat: accept allowance offers automatically on the allowee's wallet

5 participants