feat: add private payment requests - #1172

Merged
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android
Aug 28, 2026
Merged

feat: add private payment requests#1172
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds private Paykit Payment Requests to Bitkit.

Description

  1. Automatically opens incoming requests in the existing Send confirmation flow with the requesting contact, exact amount, and a Payment Request title.
  2. Keeps dismissed requests actionable through a bell, preview sheet, and full request history, with manual reopen and explicit rejection.
  3. Adds outgoing request creation from Receive for linked, saved contacts, including amount, note, expiry, queued delivery state, and sent history.
  4. Drops expired or remotely unavailable requests, keeps presentation state scoped to the active Pubky identity, and protects account changes, sheet transitions, and overlapping actions.
  5. Requires strict private resolution for requests: a consumed Private Payment List is never reused, another endpoint from that list is not attempted, and public details are never used as fallback while waiting for a newer list.
  6. Updates Paykit to 0.1.0-rc44 and adds local E2E homeserver configuration plus safe cold-start restoration for externally managed Pubky sessions.

The request payload itself remains SDK-backed and durable; Bitkit persists only encrypted, identity-scoped presentation suppression, not a duplicate request queue. Payment proofs and receipts remain out of scope.

Dependencies:

Preview

N/A — proof recordings were completed locally and are intentionally not attached to the PR.

QA Notes

Manual Tests

  • 1. Clean wallet → create Pubky profile → enable Paykit and Contact Payments → add the peer as a contact: private-capable request action appears once the Noise link is established.
  • 2. Peer creates a private request → Home: Payment Request opens automatically with the correct contact and amount.
  • 3. Payment Request → dismiss without rejecting → bell → request preview → Pay: the request remains queued, reopens, and pays successfully.
  • 4. Peer creates a second request after payment → Pay: a newer Private Payment List is used; the consumed list is not reused and no public fallback occurs.
  • 5. Receive → Send Payment Request → enter amount, note, and expiry → select linked contact → Send: request is queued once and appears in sent history.
  • 6. Incoming request → Reject: only that request becomes terminal and the private payment list is not consumed.
  • 7. Incoming request → dismiss → relaunch: request remains discoverable without automatically reopening again; expired requests disappear live.
  • 8. Cross-platform E2E: iOS creates two requests that Android receives and pays, and Android creates two requests that iOS receives and pays, on clean regtest state.

Automated Checks

  • PaykitPaymentRequestRepoTest.kt: covers mapping, eligibility, proposal delivery, rejection, expiry, identity-scoped presentation state, and action serialization.
  • PaykitSdkServiceTest.kt: covers exact identity enforcement and safe deferred session restoration.
  • AppViewModelSendFlowTest.kt: covers automatic/manual presentation, sheet transitions, identity changes, newer-list retry, strict private resolution, and payment lifecycle races.
  • PaymentRequestExpirationTest.kt: covers expiry selection and retained draft state.
  • SheetHostTest.kt: covers locked sheet dismissal and scrim input isolation during durable proposal creation.
  • Local verification passed: app compile, Android-test compile, complete dev unit suite, detekt, and git diff --check.
  • Both final cross-platform proof videos decoded end to end without errors.

Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
@greptile-apps

This comment has been minimized.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch 2 times, most recently from ba7f745 to e8183a5CompareAugust 20, 2026 10:41
ovitrif

This comment was marked as outdated.

@ovitrifovitrif added this to the 2.5.0 milestone Aug 20, 2026
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 5120677 to 7877465CompareAugust 21, 2026 01:16
@ovitrif

This comment was marked as outdated.

@piotr-iohk

This comment was marked as outdated.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from af0aaa4 to e2516e9CompareAugust 21, 2026 16:19
@ben-kaufmanben-kaufman mentioned this pull request Aug 24, 2026
2 tasks
@ovitrif
ovitrif self-requested a review August 24, 2026 16:21
ovitrif

This comment was marked as resolved.

jvsena42

This comment was marked as resolved.

ovitrif

This comment was marked as resolved.

ovitrif

This comment was marked as outdated.

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging while QA’ing with the staging public-pay pubkys — not a prod issue (Paykit UI is still hidden).

Enabling payments with contacts fail-closes with Unknown error if a saved contact’s receiver marker doesn’t parse (here: missing noise_public_key). I hit it on:

  • pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso
  • pubkynekaxdyt5ktoqyufd7cgug6qppa7999hm46wfqdzm6jqnr8tj16o

In principle the same thing could happen with any broken / outdated profile marker, not only these fixtures. Recording attached. Same surface on iOS after #676 (toast there is Private Paykit is not available).

Steps:

  1. Enable Paykit UI. Create a profile. Leave payments with contacts off.
  2. Add either (or both) staging pubkys above as contacts. They do not need to be live.
  3. Enable payments with contacts.
  4. Toast is Unknown error and the toggle stays off.
Screen.Recording.2026-08-26.at.10.12.34.mov

logs.zip

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging from QA: paying a payment request that’s larger than the wallet balance.

Same setup, two directions, both recordings attached:

  • iOS → Android (this PR): incoming sheet, Pay → toast Insufficient Spending Balance (e.g. ₿ 78,244 more needed to pay this Lightning invoice) and a red-bordered card. Dismiss then fails with Payment request operation is already in progress. Toast / error repeats; request stays on the sheet and is hard to dismiss.
Screen.Recording.2026-08-26.at.14.06.24.mov
  • Android → iOS (ldk-node Error #676, already merged):PayPlease try again. Coin selection failed. Dismiss is fine; the copy is just the wrong error.
Screen.Recording.2026-08-26.at.14.18.16.mov

Steps:

  1. Two live wallets with payments-with-contacts on. Put ~88k on the payer (split savings + spending is fine).
  2. Other wallet requests more than that total (100k Android-as-payer, 90k iOS-as-payer).
  3. On the incoming request, tap Pay, then try Dismiss.

Expected: don’t enter confirm if the amount is above spendable. On Android, Dismiss should still work after a failed Pay (no already in progress loop).

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed both QA findings in signed commit 5d39cd96a:

  • A malformed remote receiver marker is still rejected and cannot be used, but its per-contact inspection error no longer aborts global private endpoint publication for valid contacts. The regression test now covers the immediate-publication path used when enabling Contact Payments.
  • An unaffordable request now releases its manual presentation state after showing the existing insufficient-balance message. It remains pending and dismissible, is not accepted or rejected, and does not consume a Private Payment List. The regression test verifies Dismiss remains actionable and no retry starts from the old context.

The matching iOS fixes are isolated in synonymdev/bitkit-ios#684: malformed remote markers no longer block other contacts, and incoming request amounts are used during initial LN/on-chain balance validation so the standard insufficient Spending/Savings error appears before coin selection.

Local verification: 193 focused Android tests passed; detekt completed successfully with only unrelated pre-existing findings.

@piotr-iohk

piotr-iohk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Retested after 5d39cd9.

Enable — still fails. ❌

Toggle Enable payments with contactsUnknown error, toggle does not stay on. Same staging contact as before (pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso). Recording + logs attached.

This is not the noise_public_key inspect-fail anymore. Logs are PrivateUnavailable. In this run I turned the toggle off first; disable cleanup already failed with that error, then re-enable with cleanup still pending fail-closes again. Settings still maps it to Unknown error.

Same user-facing miss on iOS #684 (toast there is Private Paykit is not available).

Steps:

  1. Paykit UI on. Profile exists. Staging pubky above saved as a contact (need not be live).
  2. Payments with contacts off (or turn it off first).
  3. Enable payments with contacts.
  4. Toast is Unknown error; toggle stays off.
Screen.Recording.2026-08-27.at.10.24.05.-.android.mov

logs-android.zip

Over-balance — looks good on Android. ✅

Incoming request larger than the wallet: one Insufficient Savings / Spending toast, request stays pending and dismissible. No already in progress loop.

iOS #684 still repeats that toast (see that PR).

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed git diff master...HEAD at 56ce990 (merge-base 6ae3cb7) — 34 files, ~3.8k added lines. 10 findings inline, roughly in severity order: the ReceiveSheet start-destination change and the dropped firstError in PrivatePaykitRepo look like the two most likely to bite in practice.

Checked and looked correct: the generation/identity guards in PaykitPaymentRequestRepo (activate/refresh/synchronizeLocked/isCurrentState), creationMutex.tryLock with finally unlock in propose, expiration rescheduling, the hideSheet re-entrancy guard, SheetHost's confirmValueChange + rememberUpdatedState pairing, the scrim's pointer-consuming modifier, the stopped/recursion loop in presentNextIncomingPaykitPaymentRequest, and the sats/BTC BigDecimal conversions.

One judgement call I did not flag as a defect: the string-literal match in canDeferStaleSession is brittle against SDK message changes.

Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/components/Money.kt Outdated
Comment threadapp/src/main/res/values/strings.xml Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 56ce990 to 531578eCompareAugust 27, 2026 17:25
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed the remaining enable/disable QA path in signed, rebased commit 531578e.

The private setting now reflects the user choice independently of transient transport cleanup or immediate Noise availability:

  • Disable commits private sharing off and records failed remote cleanup as durable deferred work instead of restoring the toggle.
  • Re-enable supersedes pending removal and prepares saved contacts opportunistically; it no longer requires a live peer or immediate private endpoint publication to keep the toggle enabled.
  • Recovery persists the cleanup marker before remote removal and local pruning, so a failure between those stages remains retryable. The cached-publication fallback still recovers older states where the marker was not stored.
  • Malformed remote markers remain unusable, and there is still no private-to-public payment fallback.

Regression coverage includes deferred disable cleanup, re-enable with pending cleanup, recovery without a marker, failure after remote cleanup but before local pruning, and total versus partial marker-selection failure. The over-balance behavior that passed the retest is unchanged.

Post-rebase verification: full testDevDebugUnitTest, detekt, and compileDevDebugAndroidTestKotlin pass. The stack is rebased onto current master and all 13 PR commits are signed.

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

Retested. LGTM.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tAck

Reviewed the code manually and tested with journeys

@jvsena42
jvsena42 merged commit d644a1b into masterAug 28, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-payment-request-ui-android branch August 28, 2026 12:46
@ovitrifovitrif mentioned this pull request Aug 30, 2026
7 tasks
Sign up for freeto 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.

5 participants

@ben-kaufman@ovitrif@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: add private payment requests - #1172

Merged
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android
Aug 28, 2026
Merged

feat: add private payment requests#1172
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds private Paykit Payment Requests to Bitkit.

Description

  1. Automatically opens incoming requests in the existing Send confirmation flow with the requesting contact, exact amount, and a Payment Request title.
  2. Keeps dismissed requests actionable through a bell, preview sheet, and full request history, with manual reopen and explicit rejection.
  3. Adds outgoing request creation from Receive for linked, saved contacts, including amount, note, expiry, queued delivery state, and sent history.
  4. Drops expired or remotely unavailable requests, keeps presentation state scoped to the active Pubky identity, and protects account changes, sheet transitions, and overlapping actions.
  5. Requires strict private resolution for requests: a consumed Private Payment List is never reused, another endpoint from that list is not attempted, and public details are never used as fallback while waiting for a newer list.
  6. Updates Paykit to 0.1.0-rc44 and adds local E2E homeserver configuration plus safe cold-start restoration for externally managed Pubky sessions.

The request payload itself remains SDK-backed and durable; Bitkit persists only encrypted, identity-scoped presentation suppression, not a duplicate request queue. Payment proofs and receipts remain out of scope.

Dependencies:

Preview

N/A — proof recordings were completed locally and are intentionally not attached to the PR.

QA Notes

Manual Tests

  • 1. Clean wallet → create Pubky profile → enable Paykit and Contact Payments → add the peer as a contact: private-capable request action appears once the Noise link is established.
  • 2. Peer creates a private request → Home: Payment Request opens automatically with the correct contact and amount.
  • 3. Payment Request → dismiss without rejecting → bell → request preview → Pay: the request remains queued, reopens, and pays successfully.
  • 4. Peer creates a second request after payment → Pay: a newer Private Payment List is used; the consumed list is not reused and no public fallback occurs.
  • 5. Receive → Send Payment Request → enter amount, note, and expiry → select linked contact → Send: request is queued once and appears in sent history.
  • 6. Incoming request → Reject: only that request becomes terminal and the private payment list is not consumed.
  • 7. Incoming request → dismiss → relaunch: request remains discoverable without automatically reopening again; expired requests disappear live.
  • 8. Cross-platform E2E: iOS creates two requests that Android receives and pays, and Android creates two requests that iOS receives and pays, on clean regtest state.

Automated Checks

  • PaykitPaymentRequestRepoTest.kt: covers mapping, eligibility, proposal delivery, rejection, expiry, identity-scoped presentation state, and action serialization.
  • PaykitSdkServiceTest.kt: covers exact identity enforcement and safe deferred session restoration.
  • AppViewModelSendFlowTest.kt: covers automatic/manual presentation, sheet transitions, identity changes, newer-list retry, strict private resolution, and payment lifecycle races.
  • PaymentRequestExpirationTest.kt: covers expiry selection and retained draft state.
  • SheetHostTest.kt: covers locked sheet dismissal and scrim input isolation during durable proposal creation.
  • Local verification passed: app compile, Android-test compile, complete dev unit suite, detekt, and git diff --check.
  • Both final cross-platform proof videos decoded end to end without errors.

Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
@greptile-apps

This comment has been minimized.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch 2 times, most recently from ba7f745 to e8183a5CompareAugust 20, 2026 10:41
ovitrif

This comment was marked as outdated.

@ovitrifovitrif added this to the 2.5.0 milestone Aug 20, 2026
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 5120677 to 7877465CompareAugust 21, 2026 01:16
@ovitrif

This comment was marked as outdated.

@piotr-iohk

This comment was marked as outdated.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from af0aaa4 to e2516e9CompareAugust 21, 2026 16:19
@ben-kaufmanben-kaufman mentioned this pull request Aug 24, 2026
2 tasks
@ovitrif
ovitrif self-requested a review August 24, 2026 16:21
ovitrif

This comment was marked as resolved.

jvsena42

This comment was marked as resolved.

ovitrif

This comment was marked as resolved.

ovitrif

This comment was marked as outdated.

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging while QA’ing with the staging public-pay pubkys — not a prod issue (Paykit UI is still hidden).

Enabling payments with contacts fail-closes with Unknown error if a saved contact’s receiver marker doesn’t parse (here: missing noise_public_key). I hit it on:

  • pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso
  • pubkynekaxdyt5ktoqyufd7cgug6qppa7999hm46wfqdzm6jqnr8tj16o

In principle the same thing could happen with any broken / outdated profile marker, not only these fixtures. Recording attached. Same surface on iOS after #676 (toast there is Private Paykit is not available).

Steps:

  1. Enable Paykit UI. Create a profile. Leave payments with contacts off.
  2. Add either (or both) staging pubkys above as contacts. They do not need to be live.
  3. Enable payments with contacts.
  4. Toast is Unknown error and the toggle stays off.
Screen.Recording.2026-08-26.at.10.12.34.mov

logs.zip

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging from QA: paying a payment request that’s larger than the wallet balance.

Same setup, two directions, both recordings attached:

  • iOS → Android (this PR): incoming sheet, Pay → toast Insufficient Spending Balance (e.g. ₿ 78,244 more needed to pay this Lightning invoice) and a red-bordered card. Dismiss then fails with Payment request operation is already in progress. Toast / error repeats; request stays on the sheet and is hard to dismiss.
Screen.Recording.2026-08-26.at.14.06.24.mov
  • Android → iOS (ldk-node Error #676, already merged):PayPlease try again. Coin selection failed. Dismiss is fine; the copy is just the wrong error.
Screen.Recording.2026-08-26.at.14.18.16.mov

Steps:

  1. Two live wallets with payments-with-contacts on. Put ~88k on the payer (split savings + spending is fine).
  2. Other wallet requests more than that total (100k Android-as-payer, 90k iOS-as-payer).
  3. On the incoming request, tap Pay, then try Dismiss.

Expected: don’t enter confirm if the amount is above spendable. On Android, Dismiss should still work after a failed Pay (no already in progress loop).

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed both QA findings in signed commit 5d39cd96a:

  • A malformed remote receiver marker is still rejected and cannot be used, but its per-contact inspection error no longer aborts global private endpoint publication for valid contacts. The regression test now covers the immediate-publication path used when enabling Contact Payments.
  • An unaffordable request now releases its manual presentation state after showing the existing insufficient-balance message. It remains pending and dismissible, is not accepted or rejected, and does not consume a Private Payment List. The regression test verifies Dismiss remains actionable and no retry starts from the old context.

The matching iOS fixes are isolated in synonymdev/bitkit-ios#684: malformed remote markers no longer block other contacts, and incoming request amounts are used during initial LN/on-chain balance validation so the standard insufficient Spending/Savings error appears before coin selection.

Local verification: 193 focused Android tests passed; detekt completed successfully with only unrelated pre-existing findings.

@piotr-iohk

piotr-iohk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Retested after 5d39cd9.

Enable — still fails. ❌

Toggle Enable payments with contactsUnknown error, toggle does not stay on. Same staging contact as before (pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso). Recording + logs attached.

This is not the noise_public_key inspect-fail anymore. Logs are PrivateUnavailable. In this run I turned the toggle off first; disable cleanup already failed with that error, then re-enable with cleanup still pending fail-closes again. Settings still maps it to Unknown error.

Same user-facing miss on iOS #684 (toast there is Private Paykit is not available).

Steps:

  1. Paykit UI on. Profile exists. Staging pubky above saved as a contact (need not be live).
  2. Payments with contacts off (or turn it off first).
  3. Enable payments with contacts.
  4. Toast is Unknown error; toggle stays off.
Screen.Recording.2026-08-27.at.10.24.05.-.android.mov

logs-android.zip

Over-balance — looks good on Android. ✅

Incoming request larger than the wallet: one Insufficient Savings / Spending toast, request stays pending and dismissible. No already in progress loop.

iOS #684 still repeats that toast (see that PR).

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed git diff master...HEAD at 56ce990 (merge-base 6ae3cb7) — 34 files, ~3.8k added lines. 10 findings inline, roughly in severity order: the ReceiveSheet start-destination change and the dropped firstError in PrivatePaykitRepo look like the two most likely to bite in practice.

Checked and looked correct: the generation/identity guards in PaykitPaymentRequestRepo (activate/refresh/synchronizeLocked/isCurrentState), creationMutex.tryLock with finally unlock in propose, expiration rescheduling, the hideSheet re-entrancy guard, SheetHost's confirmValueChange + rememberUpdatedState pairing, the scrim's pointer-consuming modifier, the stopped/recursion loop in presentNextIncomingPaykitPaymentRequest, and the sats/BTC BigDecimal conversions.

One judgement call I did not flag as a defect: the string-literal match in canDeferStaleSession is brittle against SDK message changes.

Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/components/Money.kt Outdated
Comment threadapp/src/main/res/values/strings.xml Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 56ce990 to 531578eCompareAugust 27, 2026 17:25
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed the remaining enable/disable QA path in signed, rebased commit 531578e.

The private setting now reflects the user choice independently of transient transport cleanup or immediate Noise availability:

  • Disable commits private sharing off and records failed remote cleanup as durable deferred work instead of restoring the toggle.
  • Re-enable supersedes pending removal and prepares saved contacts opportunistically; it no longer requires a live peer or immediate private endpoint publication to keep the toggle enabled.
  • Recovery persists the cleanup marker before remote removal and local pruning, so a failure between those stages remains retryable. The cached-publication fallback still recovers older states where the marker was not stored.
  • Malformed remote markers remain unusable, and there is still no private-to-public payment fallback.

Regression coverage includes deferred disable cleanup, re-enable with pending cleanup, recovery without a marker, failure after remote cleanup but before local pruning, and total versus partial marker-selection failure. The over-balance behavior that passed the retest is unchanged.

Post-rebase verification: full testDevDebugUnitTest, detekt, and compileDevDebugAndroidTestKotlin pass. The stack is rebased onto current master and all 13 PR commits are signed.

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

Retested. LGTM.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tAck

Reviewed the code manually and tested with journeys

@jvsena42
jvsena42 merged commit d644a1b into masterAug 28, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-payment-request-ui-android branch August 28, 2026 12:46
@ovitrifovitrif mentioned this pull request Aug 30, 2026
7 tasks
Sign up for freeto 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.

5 participants

@ben-kaufman@ovitrif@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add private payment requests - #1172

Merged
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android
Aug 28, 2026
Merged

feat: add private payment requests#1172
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds private Paykit Payment Requests to Bitkit.

Description

  1. Automatically opens incoming requests in the existing Send confirmation flow with the requesting contact, exact amount, and a Payment Request title.
  2. Keeps dismissed requests actionable through a bell, preview sheet, and full request history, with manual reopen and explicit rejection.
  3. Adds outgoing request creation from Receive for linked, saved contacts, including amount, note, expiry, queued delivery state, and sent history.
  4. Drops expired or remotely unavailable requests, keeps presentation state scoped to the active Pubky identity, and protects account changes, sheet transitions, and overlapping actions.
  5. Requires strict private resolution for requests: a consumed Private Payment List is never reused, another endpoint from that list is not attempted, and public details are never used as fallback while waiting for a newer list.
  6. Updates Paykit to 0.1.0-rc44 and adds local E2E homeserver configuration plus safe cold-start restoration for externally managed Pubky sessions.

The request payload itself remains SDK-backed and durable; Bitkit persists only encrypted, identity-scoped presentation suppression, not a duplicate request queue. Payment proofs and receipts remain out of scope.

Dependencies:

Preview

N/A — proof recordings were completed locally and are intentionally not attached to the PR.

QA Notes

Manual Tests

  • 1. Clean wallet → create Pubky profile → enable Paykit and Contact Payments → add the peer as a contact: private-capable request action appears once the Noise link is established.
  • 2. Peer creates a private request → Home: Payment Request opens automatically with the correct contact and amount.
  • 3. Payment Request → dismiss without rejecting → bell → request preview → Pay: the request remains queued, reopens, and pays successfully.
  • 4. Peer creates a second request after payment → Pay: a newer Private Payment List is used; the consumed list is not reused and no public fallback occurs.
  • 5. Receive → Send Payment Request → enter amount, note, and expiry → select linked contact → Send: request is queued once and appears in sent history.
  • 6. Incoming request → Reject: only that request becomes terminal and the private payment list is not consumed.
  • 7. Incoming request → dismiss → relaunch: request remains discoverable without automatically reopening again; expired requests disappear live.
  • 8. Cross-platform E2E: iOS creates two requests that Android receives and pays, and Android creates two requests that iOS receives and pays, on clean regtest state.

Automated Checks

  • PaykitPaymentRequestRepoTest.kt: covers mapping, eligibility, proposal delivery, rejection, expiry, identity-scoped presentation state, and action serialization.
  • PaykitSdkServiceTest.kt: covers exact identity enforcement and safe deferred session restoration.
  • AppViewModelSendFlowTest.kt: covers automatic/manual presentation, sheet transitions, identity changes, newer-list retry, strict private resolution, and payment lifecycle races.
  • PaymentRequestExpirationTest.kt: covers expiry selection and retained draft state.
  • SheetHostTest.kt: covers locked sheet dismissal and scrim input isolation during durable proposal creation.
  • Local verification passed: app compile, Android-test compile, complete dev unit suite, detekt, and git diff --check.
  • Both final cross-platform proof videos decoded end to end without errors.

Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
@greptile-apps

This comment has been minimized.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch 2 times, most recently from ba7f745 to e8183a5CompareAugust 20, 2026 10:41
ovitrif

This comment was marked as outdated.

@ovitrifovitrif added this to the 2.5.0 milestone Aug 20, 2026
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 5120677 to 7877465CompareAugust 21, 2026 01:16
@ovitrif

This comment was marked as outdated.

@piotr-iohk

This comment was marked as outdated.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from af0aaa4 to e2516e9CompareAugust 21, 2026 16:19
@ben-kaufmanben-kaufman mentioned this pull request Aug 24, 2026
2 tasks
@ovitrif
ovitrif self-requested a review August 24, 2026 16:21
ovitrif

This comment was marked as resolved.

jvsena42

This comment was marked as resolved.

ovitrif

This comment was marked as resolved.

ovitrif

This comment was marked as outdated.

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging while QA’ing with the staging public-pay pubkys — not a prod issue (Paykit UI is still hidden).

Enabling payments with contacts fail-closes with Unknown error if a saved contact’s receiver marker doesn’t parse (here: missing noise_public_key). I hit it on:

  • pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso
  • pubkynekaxdyt5ktoqyufd7cgug6qppa7999hm46wfqdzm6jqnr8tj16o

In principle the same thing could happen with any broken / outdated profile marker, not only these fixtures. Recording attached. Same surface on iOS after #676 (toast there is Private Paykit is not available).

Steps:

  1. Enable Paykit UI. Create a profile. Leave payments with contacts off.
  2. Add either (or both) staging pubkys above as contacts. They do not need to be live.
  3. Enable payments with contacts.
  4. Toast is Unknown error and the toggle stays off.
Screen.Recording.2026-08-26.at.10.12.34.mov

logs.zip

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging from QA: paying a payment request that’s larger than the wallet balance.

Same setup, two directions, both recordings attached:

  • iOS → Android (this PR): incoming sheet, Pay → toast Insufficient Spending Balance (e.g. ₿ 78,244 more needed to pay this Lightning invoice) and a red-bordered card. Dismiss then fails with Payment request operation is already in progress. Toast / error repeats; request stays on the sheet and is hard to dismiss.
Screen.Recording.2026-08-26.at.14.06.24.mov
  • Android → iOS (ldk-node Error #676, already merged):PayPlease try again. Coin selection failed. Dismiss is fine; the copy is just the wrong error.
Screen.Recording.2026-08-26.at.14.18.16.mov

Steps:

  1. Two live wallets with payments-with-contacts on. Put ~88k on the payer (split savings + spending is fine).
  2. Other wallet requests more than that total (100k Android-as-payer, 90k iOS-as-payer).
  3. On the incoming request, tap Pay, then try Dismiss.

Expected: don’t enter confirm if the amount is above spendable. On Android, Dismiss should still work after a failed Pay (no already in progress loop).

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed both QA findings in signed commit 5d39cd96a:

  • A malformed remote receiver marker is still rejected and cannot be used, but its per-contact inspection error no longer aborts global private endpoint publication for valid contacts. The regression test now covers the immediate-publication path used when enabling Contact Payments.
  • An unaffordable request now releases its manual presentation state after showing the existing insufficient-balance message. It remains pending and dismissible, is not accepted or rejected, and does not consume a Private Payment List. The regression test verifies Dismiss remains actionable and no retry starts from the old context.

The matching iOS fixes are isolated in synonymdev/bitkit-ios#684: malformed remote markers no longer block other contacts, and incoming request amounts are used during initial LN/on-chain balance validation so the standard insufficient Spending/Savings error appears before coin selection.

Local verification: 193 focused Android tests passed; detekt completed successfully with only unrelated pre-existing findings.

@piotr-iohk

piotr-iohk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Retested after 5d39cd9.

Enable — still fails. ❌

Toggle Enable payments with contactsUnknown error, toggle does not stay on. Same staging contact as before (pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso). Recording + logs attached.

This is not the noise_public_key inspect-fail anymore. Logs are PrivateUnavailable. In this run I turned the toggle off first; disable cleanup already failed with that error, then re-enable with cleanup still pending fail-closes again. Settings still maps it to Unknown error.

Same user-facing miss on iOS #684 (toast there is Private Paykit is not available).

Steps:

  1. Paykit UI on. Profile exists. Staging pubky above saved as a contact (need not be live).
  2. Payments with contacts off (or turn it off first).
  3. Enable payments with contacts.
  4. Toast is Unknown error; toggle stays off.
Screen.Recording.2026-08-27.at.10.24.05.-.android.mov

logs-android.zip

Over-balance — looks good on Android. ✅

Incoming request larger than the wallet: one Insufficient Savings / Spending toast, request stays pending and dismissible. No already in progress loop.

iOS #684 still repeats that toast (see that PR).

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed git diff master...HEAD at 56ce990 (merge-base 6ae3cb7) — 34 files, ~3.8k added lines. 10 findings inline, roughly in severity order: the ReceiveSheet start-destination change and the dropped firstError in PrivatePaykitRepo look like the two most likely to bite in practice.

Checked and looked correct: the generation/identity guards in PaykitPaymentRequestRepo (activate/refresh/synchronizeLocked/isCurrentState), creationMutex.tryLock with finally unlock in propose, expiration rescheduling, the hideSheet re-entrancy guard, SheetHost's confirmValueChange + rememberUpdatedState pairing, the scrim's pointer-consuming modifier, the stopped/recursion loop in presentNextIncomingPaykitPaymentRequest, and the sats/BTC BigDecimal conversions.

One judgement call I did not flag as a defect: the string-literal match in canDeferStaleSession is brittle against SDK message changes.

Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/components/Money.kt Outdated
Comment threadapp/src/main/res/values/strings.xml Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 56ce990 to 531578eCompareAugust 27, 2026 17:25
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed the remaining enable/disable QA path in signed, rebased commit 531578e.

The private setting now reflects the user choice independently of transient transport cleanup or immediate Noise availability:

  • Disable commits private sharing off and records failed remote cleanup as durable deferred work instead of restoring the toggle.
  • Re-enable supersedes pending removal and prepares saved contacts opportunistically; it no longer requires a live peer or immediate private endpoint publication to keep the toggle enabled.
  • Recovery persists the cleanup marker before remote removal and local pruning, so a failure between those stages remains retryable. The cached-publication fallback still recovers older states where the marker was not stored.
  • Malformed remote markers remain unusable, and there is still no private-to-public payment fallback.

Regression coverage includes deferred disable cleanup, re-enable with pending cleanup, recovery without a marker, failure after remote cleanup but before local pruning, and total versus partial marker-selection failure. The over-balance behavior that passed the retest is unchanged.

Post-rebase verification: full testDevDebugUnitTest, detekt, and compileDevDebugAndroidTestKotlin pass. The stack is rebased onto current master and all 13 PR commits are signed.

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

Retested. LGTM.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tAck

Reviewed the code manually and tested with journeys

@jvsena42
jvsena42 merged commit d644a1b into masterAug 28, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-payment-request-ui-android branch August 28, 2026 12:46
@ovitrifovitrif mentioned this pull request Aug 30, 2026
7 tasks
Sign up for freeto 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.

5 participants

@ben-kaufman@ovitrif@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add private payment requests - #1172

Merged
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android
Aug 28, 2026
Merged

feat: add private payment requests#1172
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds private Paykit Payment Requests to Bitkit.

Description

  1. Automatically opens incoming requests in the existing Send confirmation flow with the requesting contact, exact amount, and a Payment Request title.
  2. Keeps dismissed requests actionable through a bell, preview sheet, and full request history, with manual reopen and explicit rejection.
  3. Adds outgoing request creation from Receive for linked, saved contacts, including amount, note, expiry, queued delivery state, and sent history.
  4. Drops expired or remotely unavailable requests, keeps presentation state scoped to the active Pubky identity, and protects account changes, sheet transitions, and overlapping actions.
  5. Requires strict private resolution for requests: a consumed Private Payment List is never reused, another endpoint from that list is not attempted, and public details are never used as fallback while waiting for a newer list.
  6. Updates Paykit to 0.1.0-rc44 and adds local E2E homeserver configuration plus safe cold-start restoration for externally managed Pubky sessions.

The request payload itself remains SDK-backed and durable; Bitkit persists only encrypted, identity-scoped presentation suppression, not a duplicate request queue. Payment proofs and receipts remain out of scope.

Dependencies:

Preview

N/A — proof recordings were completed locally and are intentionally not attached to the PR.

QA Notes

Manual Tests

  • 1. Clean wallet → create Pubky profile → enable Paykit and Contact Payments → add the peer as a contact: private-capable request action appears once the Noise link is established.
  • 2. Peer creates a private request → Home: Payment Request opens automatically with the correct contact and amount.
  • 3. Payment Request → dismiss without rejecting → bell → request preview → Pay: the request remains queued, reopens, and pays successfully.
  • 4. Peer creates a second request after payment → Pay: a newer Private Payment List is used; the consumed list is not reused and no public fallback occurs.
  • 5. Receive → Send Payment Request → enter amount, note, and expiry → select linked contact → Send: request is queued once and appears in sent history.
  • 6. Incoming request → Reject: only that request becomes terminal and the private payment list is not consumed.
  • 7. Incoming request → dismiss → relaunch: request remains discoverable without automatically reopening again; expired requests disappear live.
  • 8. Cross-platform E2E: iOS creates two requests that Android receives and pays, and Android creates two requests that iOS receives and pays, on clean regtest state.

Automated Checks

  • PaykitPaymentRequestRepoTest.kt: covers mapping, eligibility, proposal delivery, rejection, expiry, identity-scoped presentation state, and action serialization.
  • PaykitSdkServiceTest.kt: covers exact identity enforcement and safe deferred session restoration.
  • AppViewModelSendFlowTest.kt: covers automatic/manual presentation, sheet transitions, identity changes, newer-list retry, strict private resolution, and payment lifecycle races.
  • PaymentRequestExpirationTest.kt: covers expiry selection and retained draft state.
  • SheetHostTest.kt: covers locked sheet dismissal and scrim input isolation during durable proposal creation.
  • Local verification passed: app compile, Android-test compile, complete dev unit suite, detekt, and git diff --check.
  • Both final cross-platform proof videos decoded end to end without errors.

Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
@greptile-apps

This comment has been minimized.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch 2 times, most recently from ba7f745 to e8183a5CompareAugust 20, 2026 10:41
ovitrif

This comment was marked as outdated.

@ovitrifovitrif added this to the 2.5.0 milestone Aug 20, 2026
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 5120677 to 7877465CompareAugust 21, 2026 01:16
@ovitrif

This comment was marked as outdated.

@piotr-iohk

This comment was marked as outdated.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from af0aaa4 to e2516e9CompareAugust 21, 2026 16:19
@ben-kaufmanben-kaufman mentioned this pull request Aug 24, 2026
2 tasks
@ovitrif
ovitrif self-requested a review August 24, 2026 16:21
ovitrif

This comment was marked as resolved.

jvsena42

This comment was marked as resolved.

ovitrif

This comment was marked as resolved.

ovitrif

This comment was marked as outdated.

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging while QA’ing with the staging public-pay pubkys — not a prod issue (Paykit UI is still hidden).

Enabling payments with contacts fail-closes with Unknown error if a saved contact’s receiver marker doesn’t parse (here: missing noise_public_key). I hit it on:

  • pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso
  • pubkynekaxdyt5ktoqyufd7cgug6qppa7999hm46wfqdzm6jqnr8tj16o

In principle the same thing could happen with any broken / outdated profile marker, not only these fixtures. Recording attached. Same surface on iOS after #676 (toast there is Private Paykit is not available).

Steps:

  1. Enable Paykit UI. Create a profile. Leave payments with contacts off.
  2. Add either (or both) staging pubkys above as contacts. They do not need to be live.
  3. Enable payments with contacts.
  4. Toast is Unknown error and the toggle stays off.
Screen.Recording.2026-08-26.at.10.12.34.mov

logs.zip

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging from QA: paying a payment request that’s larger than the wallet balance.

Same setup, two directions, both recordings attached:

  • iOS → Android (this PR): incoming sheet, Pay → toast Insufficient Spending Balance (e.g. ₿ 78,244 more needed to pay this Lightning invoice) and a red-bordered card. Dismiss then fails with Payment request operation is already in progress. Toast / error repeats; request stays on the sheet and is hard to dismiss.
Screen.Recording.2026-08-26.at.14.06.24.mov
  • Android → iOS (ldk-node Error #676, already merged):PayPlease try again. Coin selection failed. Dismiss is fine; the copy is just the wrong error.
Screen.Recording.2026-08-26.at.14.18.16.mov

Steps:

  1. Two live wallets with payments-with-contacts on. Put ~88k on the payer (split savings + spending is fine).
  2. Other wallet requests more than that total (100k Android-as-payer, 90k iOS-as-payer).
  3. On the incoming request, tap Pay, then try Dismiss.

Expected: don’t enter confirm if the amount is above spendable. On Android, Dismiss should still work after a failed Pay (no already in progress loop).

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed both QA findings in signed commit 5d39cd96a:

  • A malformed remote receiver marker is still rejected and cannot be used, but its per-contact inspection error no longer aborts global private endpoint publication for valid contacts. The regression test now covers the immediate-publication path used when enabling Contact Payments.
  • An unaffordable request now releases its manual presentation state after showing the existing insufficient-balance message. It remains pending and dismissible, is not accepted or rejected, and does not consume a Private Payment List. The regression test verifies Dismiss remains actionable and no retry starts from the old context.

The matching iOS fixes are isolated in synonymdev/bitkit-ios#684: malformed remote markers no longer block other contacts, and incoming request amounts are used during initial LN/on-chain balance validation so the standard insufficient Spending/Savings error appears before coin selection.

Local verification: 193 focused Android tests passed; detekt completed successfully with only unrelated pre-existing findings.

@piotr-iohk

piotr-iohk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Retested after 5d39cd9.

Enable — still fails. ❌

Toggle Enable payments with contactsUnknown error, toggle does not stay on. Same staging contact as before (pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso). Recording + logs attached.

This is not the noise_public_key inspect-fail anymore. Logs are PrivateUnavailable. In this run I turned the toggle off first; disable cleanup already failed with that error, then re-enable with cleanup still pending fail-closes again. Settings still maps it to Unknown error.

Same user-facing miss on iOS #684 (toast there is Private Paykit is not available).

Steps:

  1. Paykit UI on. Profile exists. Staging pubky above saved as a contact (need not be live).
  2. Payments with contacts off (or turn it off first).
  3. Enable payments with contacts.
  4. Toast is Unknown error; toggle stays off.
Screen.Recording.2026-08-27.at.10.24.05.-.android.mov

logs-android.zip

Over-balance — looks good on Android. ✅

Incoming request larger than the wallet: one Insufficient Savings / Spending toast, request stays pending and dismissible. No already in progress loop.

iOS #684 still repeats that toast (see that PR).

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed git diff master...HEAD at 56ce990 (merge-base 6ae3cb7) — 34 files, ~3.8k added lines. 10 findings inline, roughly in severity order: the ReceiveSheet start-destination change and the dropped firstError in PrivatePaykitRepo look like the two most likely to bite in practice.

Checked and looked correct: the generation/identity guards in PaykitPaymentRequestRepo (activate/refresh/synchronizeLocked/isCurrentState), creationMutex.tryLock with finally unlock in propose, expiration rescheduling, the hideSheet re-entrancy guard, SheetHost's confirmValueChange + rememberUpdatedState pairing, the scrim's pointer-consuming modifier, the stopped/recursion loop in presentNextIncomingPaykitPaymentRequest, and the sats/BTC BigDecimal conversions.

One judgement call I did not flag as a defect: the string-literal match in canDeferStaleSession is brittle against SDK message changes.

Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/components/Money.kt Outdated
Comment threadapp/src/main/res/values/strings.xml Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 56ce990 to 531578eCompareAugust 27, 2026 17:25
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed the remaining enable/disable QA path in signed, rebased commit 531578e.

The private setting now reflects the user choice independently of transient transport cleanup or immediate Noise availability:

  • Disable commits private sharing off and records failed remote cleanup as durable deferred work instead of restoring the toggle.
  • Re-enable supersedes pending removal and prepares saved contacts opportunistically; it no longer requires a live peer or immediate private endpoint publication to keep the toggle enabled.
  • Recovery persists the cleanup marker before remote removal and local pruning, so a failure between those stages remains retryable. The cached-publication fallback still recovers older states where the marker was not stored.
  • Malformed remote markers remain unusable, and there is still no private-to-public payment fallback.

Regression coverage includes deferred disable cleanup, re-enable with pending cleanup, recovery without a marker, failure after remote cleanup but before local pruning, and total versus partial marker-selection failure. The over-balance behavior that passed the retest is unchanged.

Post-rebase verification: full testDevDebugUnitTest, detekt, and compileDevDebugAndroidTestKotlin pass. The stack is rebased onto current master and all 13 PR commits are signed.

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

Retested. LGTM.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tAck

Reviewed the code manually and tested with journeys

@jvsena42
jvsena42 merged commit d644a1b into masterAug 28, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-payment-request-ui-android branch August 28, 2026 12:46
@ovitrifovitrif mentioned this pull request Aug 30, 2026
7 tasks
Sign up for freeto 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.

5 participants

@ben-kaufman@ovitrif@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: add private payment requests - #1172

Merged
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android
Aug 28, 2026
Merged

feat: add private payment requests#1172
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds private Paykit Payment Requests to Bitkit.

Description

  1. Automatically opens incoming requests in the existing Send confirmation flow with the requesting contact, exact amount, and a Payment Request title.
  2. Keeps dismissed requests actionable through a bell, preview sheet, and full request history, with manual reopen and explicit rejection.
  3. Adds outgoing request creation from Receive for linked, saved contacts, including amount, note, expiry, queued delivery state, and sent history.
  4. Drops expired or remotely unavailable requests, keeps presentation state scoped to the active Pubky identity, and protects account changes, sheet transitions, and overlapping actions.
  5. Requires strict private resolution for requests: a consumed Private Payment List is never reused, another endpoint from that list is not attempted, and public details are never used as fallback while waiting for a newer list.
  6. Updates Paykit to 0.1.0-rc44 and adds local E2E homeserver configuration plus safe cold-start restoration for externally managed Pubky sessions.

The request payload itself remains SDK-backed and durable; Bitkit persists only encrypted, identity-scoped presentation suppression, not a duplicate request queue. Payment proofs and receipts remain out of scope.

Dependencies:

Preview

N/A — proof recordings were completed locally and are intentionally not attached to the PR.

QA Notes

Manual Tests

  • 1. Clean wallet → create Pubky profile → enable Paykit and Contact Payments → add the peer as a contact: private-capable request action appears once the Noise link is established.
  • 2. Peer creates a private request → Home: Payment Request opens automatically with the correct contact and amount.
  • 3. Payment Request → dismiss without rejecting → bell → request preview → Pay: the request remains queued, reopens, and pays successfully.
  • 4. Peer creates a second request after payment → Pay: a newer Private Payment List is used; the consumed list is not reused and no public fallback occurs.
  • 5. Receive → Send Payment Request → enter amount, note, and expiry → select linked contact → Send: request is queued once and appears in sent history.
  • 6. Incoming request → Reject: only that request becomes terminal and the private payment list is not consumed.
  • 7. Incoming request → dismiss → relaunch: request remains discoverable without automatically reopening again; expired requests disappear live.
  • 8. Cross-platform E2E: iOS creates two requests that Android receives and pays, and Android creates two requests that iOS receives and pays, on clean regtest state.

Automated Checks

  • PaykitPaymentRequestRepoTest.kt: covers mapping, eligibility, proposal delivery, rejection, expiry, identity-scoped presentation state, and action serialization.
  • PaykitSdkServiceTest.kt: covers exact identity enforcement and safe deferred session restoration.
  • AppViewModelSendFlowTest.kt: covers automatic/manual presentation, sheet transitions, identity changes, newer-list retry, strict private resolution, and payment lifecycle races.
  • PaymentRequestExpirationTest.kt: covers expiry selection and retained draft state.
  • SheetHostTest.kt: covers locked sheet dismissal and scrim input isolation during durable proposal creation.
  • Local verification passed: app compile, Android-test compile, complete dev unit suite, detekt, and git diff --check.
  • Both final cross-platform proof videos decoded end to end without errors.

Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
@greptile-apps

This comment has been minimized.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch 2 times, most recently from ba7f745 to e8183a5CompareAugust 20, 2026 10:41
ovitrif

This comment was marked as outdated.

@ovitrifovitrif added this to the 2.5.0 milestone Aug 20, 2026
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 5120677 to 7877465CompareAugust 21, 2026 01:16
@ovitrif

This comment was marked as outdated.

@piotr-iohk

This comment was marked as outdated.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from af0aaa4 to e2516e9CompareAugust 21, 2026 16:19
@ben-kaufmanben-kaufman mentioned this pull request Aug 24, 2026
2 tasks
@ovitrif
ovitrif self-requested a review August 24, 2026 16:21
ovitrif

This comment was marked as resolved.

jvsena42

This comment was marked as resolved.

ovitrif

This comment was marked as resolved.

ovitrif

This comment was marked as outdated.

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging while QA’ing with the staging public-pay pubkys — not a prod issue (Paykit UI is still hidden).

Enabling payments with contacts fail-closes with Unknown error if a saved contact’s receiver marker doesn’t parse (here: missing noise_public_key). I hit it on:

  • pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso
  • pubkynekaxdyt5ktoqyufd7cgug6qppa7999hm46wfqdzm6jqnr8tj16o

In principle the same thing could happen with any broken / outdated profile marker, not only these fixtures. Recording attached. Same surface on iOS after #676 (toast there is Private Paykit is not available).

Steps:

  1. Enable Paykit UI. Create a profile. Leave payments with contacts off.
  2. Add either (or both) staging pubkys above as contacts. They do not need to be live.
  3. Enable payments with contacts.
  4. Toast is Unknown error and the toggle stays off.
Screen.Recording.2026-08-26.at.10.12.34.mov

logs.zip

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging from QA: paying a payment request that’s larger than the wallet balance.

Same setup, two directions, both recordings attached:

  • iOS → Android (this PR): incoming sheet, Pay → toast Insufficient Spending Balance (e.g. ₿ 78,244 more needed to pay this Lightning invoice) and a red-bordered card. Dismiss then fails with Payment request operation is already in progress. Toast / error repeats; request stays on the sheet and is hard to dismiss.
Screen.Recording.2026-08-26.at.14.06.24.mov
  • Android → iOS (ldk-node Error #676, already merged):PayPlease try again. Coin selection failed. Dismiss is fine; the copy is just the wrong error.
Screen.Recording.2026-08-26.at.14.18.16.mov

Steps:

  1. Two live wallets with payments-with-contacts on. Put ~88k on the payer (split savings + spending is fine).
  2. Other wallet requests more than that total (100k Android-as-payer, 90k iOS-as-payer).
  3. On the incoming request, tap Pay, then try Dismiss.

Expected: don’t enter confirm if the amount is above spendable. On Android, Dismiss should still work after a failed Pay (no already in progress loop).

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed both QA findings in signed commit 5d39cd96a:

  • A malformed remote receiver marker is still rejected and cannot be used, but its per-contact inspection error no longer aborts global private endpoint publication for valid contacts. The regression test now covers the immediate-publication path used when enabling Contact Payments.
  • An unaffordable request now releases its manual presentation state after showing the existing insufficient-balance message. It remains pending and dismissible, is not accepted or rejected, and does not consume a Private Payment List. The regression test verifies Dismiss remains actionable and no retry starts from the old context.

The matching iOS fixes are isolated in synonymdev/bitkit-ios#684: malformed remote markers no longer block other contacts, and incoming request amounts are used during initial LN/on-chain balance validation so the standard insufficient Spending/Savings error appears before coin selection.

Local verification: 193 focused Android tests passed; detekt completed successfully with only unrelated pre-existing findings.

@piotr-iohk

piotr-iohk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Retested after 5d39cd9.

Enable — still fails. ❌

Toggle Enable payments with contactsUnknown error, toggle does not stay on. Same staging contact as before (pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso). Recording + logs attached.

This is not the noise_public_key inspect-fail anymore. Logs are PrivateUnavailable. In this run I turned the toggle off first; disable cleanup already failed with that error, then re-enable with cleanup still pending fail-closes again. Settings still maps it to Unknown error.

Same user-facing miss on iOS #684 (toast there is Private Paykit is not available).

Steps:

  1. Paykit UI on. Profile exists. Staging pubky above saved as a contact (need not be live).
  2. Payments with contacts off (or turn it off first).
  3. Enable payments with contacts.
  4. Toast is Unknown error; toggle stays off.
Screen.Recording.2026-08-27.at.10.24.05.-.android.mov

logs-android.zip

Over-balance — looks good on Android. ✅

Incoming request larger than the wallet: one Insufficient Savings / Spending toast, request stays pending and dismissible. No already in progress loop.

iOS #684 still repeats that toast (see that PR).

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed git diff master...HEAD at 56ce990 (merge-base 6ae3cb7) — 34 files, ~3.8k added lines. 10 findings inline, roughly in severity order: the ReceiveSheet start-destination change and the dropped firstError in PrivatePaykitRepo look like the two most likely to bite in practice.

Checked and looked correct: the generation/identity guards in PaykitPaymentRequestRepo (activate/refresh/synchronizeLocked/isCurrentState), creationMutex.tryLock with finally unlock in propose, expiration rescheduling, the hideSheet re-entrancy guard, SheetHost's confirmValueChange + rememberUpdatedState pairing, the scrim's pointer-consuming modifier, the stopped/recursion loop in presentNextIncomingPaykitPaymentRequest, and the sats/BTC BigDecimal conversions.

One judgement call I did not flag as a defect: the string-literal match in canDeferStaleSession is brittle against SDK message changes.

Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/components/Money.kt Outdated
Comment threadapp/src/main/res/values/strings.xml Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 56ce990 to 531578eCompareAugust 27, 2026 17:25
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed the remaining enable/disable QA path in signed, rebased commit 531578e.

The private setting now reflects the user choice independently of transient transport cleanup or immediate Noise availability:

  • Disable commits private sharing off and records failed remote cleanup as durable deferred work instead of restoring the toggle.
  • Re-enable supersedes pending removal and prepares saved contacts opportunistically; it no longer requires a live peer or immediate private endpoint publication to keep the toggle enabled.
  • Recovery persists the cleanup marker before remote removal and local pruning, so a failure between those stages remains retryable. The cached-publication fallback still recovers older states where the marker was not stored.
  • Malformed remote markers remain unusable, and there is still no private-to-public payment fallback.

Regression coverage includes deferred disable cleanup, re-enable with pending cleanup, recovery without a marker, failure after remote cleanup but before local pruning, and total versus partial marker-selection failure. The over-balance behavior that passed the retest is unchanged.

Post-rebase verification: full testDevDebugUnitTest, detekt, and compileDevDebugAndroidTestKotlin pass. The stack is rebased onto current master and all 13 PR commits are signed.

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

Retested. LGTM.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tAck

Reviewed the code manually and tested with journeys

@jvsena42
jvsena42 merged commit d644a1b into masterAug 28, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-payment-request-ui-android branch August 28, 2026 12:46
@ovitrifovitrif mentioned this pull request Aug 30, 2026
7 tasks
Sign up for freeto 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.

5 participants

@ben-kaufman@ovitrif@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add private payment requests - #1172

Merged
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android
Aug 28, 2026
Merged

feat: add private payment requests#1172
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds private Paykit Payment Requests to Bitkit.

Description

  1. Automatically opens incoming requests in the existing Send confirmation flow with the requesting contact, exact amount, and a Payment Request title.
  2. Keeps dismissed requests actionable through a bell, preview sheet, and full request history, with manual reopen and explicit rejection.
  3. Adds outgoing request creation from Receive for linked, saved contacts, including amount, note, expiry, queued delivery state, and sent history.
  4. Drops expired or remotely unavailable requests, keeps presentation state scoped to the active Pubky identity, and protects account changes, sheet transitions, and overlapping actions.
  5. Requires strict private resolution for requests: a consumed Private Payment List is never reused, another endpoint from that list is not attempted, and public details are never used as fallback while waiting for a newer list.
  6. Updates Paykit to 0.1.0-rc44 and adds local E2E homeserver configuration plus safe cold-start restoration for externally managed Pubky sessions.

The request payload itself remains SDK-backed and durable; Bitkit persists only encrypted, identity-scoped presentation suppression, not a duplicate request queue. Payment proofs and receipts remain out of scope.

Dependencies:

Preview

N/A — proof recordings were completed locally and are intentionally not attached to the PR.

QA Notes

Manual Tests

  • 1. Clean wallet → create Pubky profile → enable Paykit and Contact Payments → add the peer as a contact: private-capable request action appears once the Noise link is established.
  • 2. Peer creates a private request → Home: Payment Request opens automatically with the correct contact and amount.
  • 3. Payment Request → dismiss without rejecting → bell → request preview → Pay: the request remains queued, reopens, and pays successfully.
  • 4. Peer creates a second request after payment → Pay: a newer Private Payment List is used; the consumed list is not reused and no public fallback occurs.
  • 5. Receive → Send Payment Request → enter amount, note, and expiry → select linked contact → Send: request is queued once and appears in sent history.
  • 6. Incoming request → Reject: only that request becomes terminal and the private payment list is not consumed.
  • 7. Incoming request → dismiss → relaunch: request remains discoverable without automatically reopening again; expired requests disappear live.
  • 8. Cross-platform E2E: iOS creates two requests that Android receives and pays, and Android creates two requests that iOS receives and pays, on clean regtest state.

Automated Checks

  • PaykitPaymentRequestRepoTest.kt: covers mapping, eligibility, proposal delivery, rejection, expiry, identity-scoped presentation state, and action serialization.
  • PaykitSdkServiceTest.kt: covers exact identity enforcement and safe deferred session restoration.
  • AppViewModelSendFlowTest.kt: covers automatic/manual presentation, sheet transitions, identity changes, newer-list retry, strict private resolution, and payment lifecycle races.
  • PaymentRequestExpirationTest.kt: covers expiry selection and retained draft state.
  • SheetHostTest.kt: covers locked sheet dismissal and scrim input isolation during durable proposal creation.
  • Local verification passed: app compile, Android-test compile, complete dev unit suite, detekt, and git diff --check.
  • Both final cross-platform proof videos decoded end to end without errors.

Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
@greptile-apps

This comment has been minimized.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch 2 times, most recently from ba7f745 to e8183a5CompareAugust 20, 2026 10:41
ovitrif

This comment was marked as outdated.

@ovitrifovitrif added this to the 2.5.0 milestone Aug 20, 2026
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 5120677 to 7877465CompareAugust 21, 2026 01:16
@ovitrif

This comment was marked as outdated.

@piotr-iohk

This comment was marked as outdated.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from af0aaa4 to e2516e9CompareAugust 21, 2026 16:19
@ben-kaufmanben-kaufman mentioned this pull request Aug 24, 2026
2 tasks
@ovitrif
ovitrif self-requested a review August 24, 2026 16:21
ovitrif

This comment was marked as resolved.

jvsena42

This comment was marked as resolved.

ovitrif

This comment was marked as resolved.

ovitrif

This comment was marked as outdated.

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging while QA’ing with the staging public-pay pubkys — not a prod issue (Paykit UI is still hidden).

Enabling payments with contacts fail-closes with Unknown error if a saved contact’s receiver marker doesn’t parse (here: missing noise_public_key). I hit it on:

  • pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso
  • pubkynekaxdyt5ktoqyufd7cgug6qppa7999hm46wfqdzm6jqnr8tj16o

In principle the same thing could happen with any broken / outdated profile marker, not only these fixtures. Recording attached. Same surface on iOS after #676 (toast there is Private Paykit is not available).

Steps:

  1. Enable Paykit UI. Create a profile. Leave payments with contacts off.
  2. Add either (or both) staging pubkys above as contacts. They do not need to be live.
  3. Enable payments with contacts.
  4. Toast is Unknown error and the toggle stays off.
Screen.Recording.2026-08-26.at.10.12.34.mov

logs.zip

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging from QA: paying a payment request that’s larger than the wallet balance.

Same setup, two directions, both recordings attached:

  • iOS → Android (this PR): incoming sheet, Pay → toast Insufficient Spending Balance (e.g. ₿ 78,244 more needed to pay this Lightning invoice) and a red-bordered card. Dismiss then fails with Payment request operation is already in progress. Toast / error repeats; request stays on the sheet and is hard to dismiss.
Screen.Recording.2026-08-26.at.14.06.24.mov
  • Android → iOS (ldk-node Error #676, already merged):PayPlease try again. Coin selection failed. Dismiss is fine; the copy is just the wrong error.
Screen.Recording.2026-08-26.at.14.18.16.mov

Steps:

  1. Two live wallets with payments-with-contacts on. Put ~88k on the payer (split savings + spending is fine).
  2. Other wallet requests more than that total (100k Android-as-payer, 90k iOS-as-payer).
  3. On the incoming request, tap Pay, then try Dismiss.

Expected: don’t enter confirm if the amount is above spendable. On Android, Dismiss should still work after a failed Pay (no already in progress loop).

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed both QA findings in signed commit 5d39cd96a:

  • A malformed remote receiver marker is still rejected and cannot be used, but its per-contact inspection error no longer aborts global private endpoint publication for valid contacts. The regression test now covers the immediate-publication path used when enabling Contact Payments.
  • An unaffordable request now releases its manual presentation state after showing the existing insufficient-balance message. It remains pending and dismissible, is not accepted or rejected, and does not consume a Private Payment List. The regression test verifies Dismiss remains actionable and no retry starts from the old context.

The matching iOS fixes are isolated in synonymdev/bitkit-ios#684: malformed remote markers no longer block other contacts, and incoming request amounts are used during initial LN/on-chain balance validation so the standard insufficient Spending/Savings error appears before coin selection.

Local verification: 193 focused Android tests passed; detekt completed successfully with only unrelated pre-existing findings.

@piotr-iohk

piotr-iohk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Retested after 5d39cd9.

Enable — still fails. ❌

Toggle Enable payments with contactsUnknown error, toggle does not stay on. Same staging contact as before (pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso). Recording + logs attached.

This is not the noise_public_key inspect-fail anymore. Logs are PrivateUnavailable. In this run I turned the toggle off first; disable cleanup already failed with that error, then re-enable with cleanup still pending fail-closes again. Settings still maps it to Unknown error.

Same user-facing miss on iOS #684 (toast there is Private Paykit is not available).

Steps:

  1. Paykit UI on. Profile exists. Staging pubky above saved as a contact (need not be live).
  2. Payments with contacts off (or turn it off first).
  3. Enable payments with contacts.
  4. Toast is Unknown error; toggle stays off.
Screen.Recording.2026-08-27.at.10.24.05.-.android.mov

logs-android.zip

Over-balance — looks good on Android. ✅

Incoming request larger than the wallet: one Insufficient Savings / Spending toast, request stays pending and dismissible. No already in progress loop.

iOS #684 still repeats that toast (see that PR).

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed git diff master...HEAD at 56ce990 (merge-base 6ae3cb7) — 34 files, ~3.8k added lines. 10 findings inline, roughly in severity order: the ReceiveSheet start-destination change and the dropped firstError in PrivatePaykitRepo look like the two most likely to bite in practice.

Checked and looked correct: the generation/identity guards in PaykitPaymentRequestRepo (activate/refresh/synchronizeLocked/isCurrentState), creationMutex.tryLock with finally unlock in propose, expiration rescheduling, the hideSheet re-entrancy guard, SheetHost's confirmValueChange + rememberUpdatedState pairing, the scrim's pointer-consuming modifier, the stopped/recursion loop in presentNextIncomingPaykitPaymentRequest, and the sats/BTC BigDecimal conversions.

One judgement call I did not flag as a defect: the string-literal match in canDeferStaleSession is brittle against SDK message changes.

Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/components/Money.kt Outdated
Comment threadapp/src/main/res/values/strings.xml Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 56ce990 to 531578eCompareAugust 27, 2026 17:25
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed the remaining enable/disable QA path in signed, rebased commit 531578e.

The private setting now reflects the user choice independently of transient transport cleanup or immediate Noise availability:

  • Disable commits private sharing off and records failed remote cleanup as durable deferred work instead of restoring the toggle.
  • Re-enable supersedes pending removal and prepares saved contacts opportunistically; it no longer requires a live peer or immediate private endpoint publication to keep the toggle enabled.
  • Recovery persists the cleanup marker before remote removal and local pruning, so a failure between those stages remains retryable. The cached-publication fallback still recovers older states where the marker was not stored.
  • Malformed remote markers remain unusable, and there is still no private-to-public payment fallback.

Regression coverage includes deferred disable cleanup, re-enable with pending cleanup, recovery without a marker, failure after remote cleanup but before local pruning, and total versus partial marker-selection failure. The over-balance behavior that passed the retest is unchanged.

Post-rebase verification: full testDevDebugUnitTest, detekt, and compileDevDebugAndroidTestKotlin pass. The stack is rebased onto current master and all 13 PR commits are signed.

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

Retested. LGTM.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tAck

Reviewed the code manually and tested with journeys

@jvsena42
jvsena42 merged commit d644a1b into masterAug 28, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-payment-request-ui-android branch August 28, 2026 12:46
@ovitrifovitrif mentioned this pull request Aug 30, 2026
7 tasks
Sign up for freeto 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.

5 participants

@ben-kaufman@ovitrif@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add private payment requests - #1172

Merged
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android
Aug 28, 2026
Merged

feat: add private payment requests#1172
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds private Paykit Payment Requests to Bitkit.

Description

  1. Automatically opens incoming requests in the existing Send confirmation flow with the requesting contact, exact amount, and a Payment Request title.
  2. Keeps dismissed requests actionable through a bell, preview sheet, and full request history, with manual reopen and explicit rejection.
  3. Adds outgoing request creation from Receive for linked, saved contacts, including amount, note, expiry, queued delivery state, and sent history.
  4. Drops expired or remotely unavailable requests, keeps presentation state scoped to the active Pubky identity, and protects account changes, sheet transitions, and overlapping actions.
  5. Requires strict private resolution for requests: a consumed Private Payment List is never reused, another endpoint from that list is not attempted, and public details are never used as fallback while waiting for a newer list.
  6. Updates Paykit to 0.1.0-rc44 and adds local E2E homeserver configuration plus safe cold-start restoration for externally managed Pubky sessions.

The request payload itself remains SDK-backed and durable; Bitkit persists only encrypted, identity-scoped presentation suppression, not a duplicate request queue. Payment proofs and receipts remain out of scope.

Dependencies:

Preview

N/A — proof recordings were completed locally and are intentionally not attached to the PR.

QA Notes

Manual Tests

  • 1. Clean wallet → create Pubky profile → enable Paykit and Contact Payments → add the peer as a contact: private-capable request action appears once the Noise link is established.
  • 2. Peer creates a private request → Home: Payment Request opens automatically with the correct contact and amount.
  • 3. Payment Request → dismiss without rejecting → bell → request preview → Pay: the request remains queued, reopens, and pays successfully.
  • 4. Peer creates a second request after payment → Pay: a newer Private Payment List is used; the consumed list is not reused and no public fallback occurs.
  • 5. Receive → Send Payment Request → enter amount, note, and expiry → select linked contact → Send: request is queued once and appears in sent history.
  • 6. Incoming request → Reject: only that request becomes terminal and the private payment list is not consumed.
  • 7. Incoming request → dismiss → relaunch: request remains discoverable without automatically reopening again; expired requests disappear live.
  • 8. Cross-platform E2E: iOS creates two requests that Android receives and pays, and Android creates two requests that iOS receives and pays, on clean regtest state.

Automated Checks

  • PaykitPaymentRequestRepoTest.kt: covers mapping, eligibility, proposal delivery, rejection, expiry, identity-scoped presentation state, and action serialization.
  • PaykitSdkServiceTest.kt: covers exact identity enforcement and safe deferred session restoration.
  • AppViewModelSendFlowTest.kt: covers automatic/manual presentation, sheet transitions, identity changes, newer-list retry, strict private resolution, and payment lifecycle races.
  • PaymentRequestExpirationTest.kt: covers expiry selection and retained draft state.
  • SheetHostTest.kt: covers locked sheet dismissal and scrim input isolation during durable proposal creation.
  • Local verification passed: app compile, Android-test compile, complete dev unit suite, detekt, and git diff --check.
  • Both final cross-platform proof videos decoded end to end without errors.

Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
@greptile-apps

This comment has been minimized.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch 2 times, most recently from ba7f745 to e8183a5CompareAugust 20, 2026 10:41
ovitrif

This comment was marked as outdated.

@ovitrifovitrif added this to the 2.5.0 milestone Aug 20, 2026
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 5120677 to 7877465CompareAugust 21, 2026 01:16
@ovitrif

This comment was marked as outdated.

@piotr-iohk

This comment was marked as outdated.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from af0aaa4 to e2516e9CompareAugust 21, 2026 16:19
@ben-kaufmanben-kaufman mentioned this pull request Aug 24, 2026
2 tasks
@ovitrif
ovitrif self-requested a review August 24, 2026 16:21
ovitrif

This comment was marked as resolved.

jvsena42

This comment was marked as resolved.

ovitrif

This comment was marked as resolved.

ovitrif

This comment was marked as outdated.

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging while QA’ing with the staging public-pay pubkys — not a prod issue (Paykit UI is still hidden).

Enabling payments with contacts fail-closes with Unknown error if a saved contact’s receiver marker doesn’t parse (here: missing noise_public_key). I hit it on:

  • pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso
  • pubkynekaxdyt5ktoqyufd7cgug6qppa7999hm46wfqdzm6jqnr8tj16o

In principle the same thing could happen with any broken / outdated profile marker, not only these fixtures. Recording attached. Same surface on iOS after #676 (toast there is Private Paykit is not available).

Steps:

  1. Enable Paykit UI. Create a profile. Leave payments with contacts off.
  2. Add either (or both) staging pubkys above as contacts. They do not need to be live.
  3. Enable payments with contacts.
  4. Toast is Unknown error and the toggle stays off.
Screen.Recording.2026-08-26.at.10.12.34.mov

logs.zip

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging from QA: paying a payment request that’s larger than the wallet balance.

Same setup, two directions, both recordings attached:

  • iOS → Android (this PR): incoming sheet, Pay → toast Insufficient Spending Balance (e.g. ₿ 78,244 more needed to pay this Lightning invoice) and a red-bordered card. Dismiss then fails with Payment request operation is already in progress. Toast / error repeats; request stays on the sheet and is hard to dismiss.
Screen.Recording.2026-08-26.at.14.06.24.mov
  • Android → iOS (ldk-node Error #676, already merged):PayPlease try again. Coin selection failed. Dismiss is fine; the copy is just the wrong error.
Screen.Recording.2026-08-26.at.14.18.16.mov

Steps:

  1. Two live wallets with payments-with-contacts on. Put ~88k on the payer (split savings + spending is fine).
  2. Other wallet requests more than that total (100k Android-as-payer, 90k iOS-as-payer).
  3. On the incoming request, tap Pay, then try Dismiss.

Expected: don’t enter confirm if the amount is above spendable. On Android, Dismiss should still work after a failed Pay (no already in progress loop).

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed both QA findings in signed commit 5d39cd96a:

  • A malformed remote receiver marker is still rejected and cannot be used, but its per-contact inspection error no longer aborts global private endpoint publication for valid contacts. The regression test now covers the immediate-publication path used when enabling Contact Payments.
  • An unaffordable request now releases its manual presentation state after showing the existing insufficient-balance message. It remains pending and dismissible, is not accepted or rejected, and does not consume a Private Payment List. The regression test verifies Dismiss remains actionable and no retry starts from the old context.

The matching iOS fixes are isolated in synonymdev/bitkit-ios#684: malformed remote markers no longer block other contacts, and incoming request amounts are used during initial LN/on-chain balance validation so the standard insufficient Spending/Savings error appears before coin selection.

Local verification: 193 focused Android tests passed; detekt completed successfully with only unrelated pre-existing findings.

@piotr-iohk

piotr-iohk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Retested after 5d39cd9.

Enable — still fails. ❌

Toggle Enable payments with contactsUnknown error, toggle does not stay on. Same staging contact as before (pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso). Recording + logs attached.

This is not the noise_public_key inspect-fail anymore. Logs are PrivateUnavailable. In this run I turned the toggle off first; disable cleanup already failed with that error, then re-enable with cleanup still pending fail-closes again. Settings still maps it to Unknown error.

Same user-facing miss on iOS #684 (toast there is Private Paykit is not available).

Steps:

  1. Paykit UI on. Profile exists. Staging pubky above saved as a contact (need not be live).
  2. Payments with contacts off (or turn it off first).
  3. Enable payments with contacts.
  4. Toast is Unknown error; toggle stays off.
Screen.Recording.2026-08-27.at.10.24.05.-.android.mov

logs-android.zip

Over-balance — looks good on Android. ✅

Incoming request larger than the wallet: one Insufficient Savings / Spending toast, request stays pending and dismissible. No already in progress loop.

iOS #684 still repeats that toast (see that PR).

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed git diff master...HEAD at 56ce990 (merge-base 6ae3cb7) — 34 files, ~3.8k added lines. 10 findings inline, roughly in severity order: the ReceiveSheet start-destination change and the dropped firstError in PrivatePaykitRepo look like the two most likely to bite in practice.

Checked and looked correct: the generation/identity guards in PaykitPaymentRequestRepo (activate/refresh/synchronizeLocked/isCurrentState), creationMutex.tryLock with finally unlock in propose, expiration rescheduling, the hideSheet re-entrancy guard, SheetHost's confirmValueChange + rememberUpdatedState pairing, the scrim's pointer-consuming modifier, the stopped/recursion loop in presentNextIncomingPaykitPaymentRequest, and the sats/BTC BigDecimal conversions.

One judgement call I did not flag as a defect: the string-literal match in canDeferStaleSession is brittle against SDK message changes.

Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/components/Money.kt Outdated
Comment threadapp/src/main/res/values/strings.xml Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 56ce990 to 531578eCompareAugust 27, 2026 17:25
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed the remaining enable/disable QA path in signed, rebased commit 531578e.

The private setting now reflects the user choice independently of transient transport cleanup or immediate Noise availability:

  • Disable commits private sharing off and records failed remote cleanup as durable deferred work instead of restoring the toggle.
  • Re-enable supersedes pending removal and prepares saved contacts opportunistically; it no longer requires a live peer or immediate private endpoint publication to keep the toggle enabled.
  • Recovery persists the cleanup marker before remote removal and local pruning, so a failure between those stages remains retryable. The cached-publication fallback still recovers older states where the marker was not stored.
  • Malformed remote markers remain unusable, and there is still no private-to-public payment fallback.

Regression coverage includes deferred disable cleanup, re-enable with pending cleanup, recovery without a marker, failure after remote cleanup but before local pruning, and total versus partial marker-selection failure. The over-balance behavior that passed the retest is unchanged.

Post-rebase verification: full testDevDebugUnitTest, detekt, and compileDevDebugAndroidTestKotlin pass. The stack is rebased onto current master and all 13 PR commits are signed.

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

Retested. LGTM.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tAck

Reviewed the code manually and tested with journeys

@jvsena42
jvsena42 merged commit d644a1b into masterAug 28, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-payment-request-ui-android branch August 28, 2026 12:46
@ovitrifovitrif mentioned this pull request Aug 30, 2026
7 tasks
Sign up for freeto 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.

5 participants

@ben-kaufman@ovitrif@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: add private payment requests - #1172

Merged
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android
Aug 28, 2026
Merged

feat: add private payment requests#1172
jvsena42 merged 13 commits into
masterfrom
codex/paykit-payment-request-ui-android

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds private Paykit Payment Requests to Bitkit.

Description

  1. Automatically opens incoming requests in the existing Send confirmation flow with the requesting contact, exact amount, and a Payment Request title.
  2. Keeps dismissed requests actionable through a bell, preview sheet, and full request history, with manual reopen and explicit rejection.
  3. Adds outgoing request creation from Receive for linked, saved contacts, including amount, note, expiry, queued delivery state, and sent history.
  4. Drops expired or remotely unavailable requests, keeps presentation state scoped to the active Pubky identity, and protects account changes, sheet transitions, and overlapping actions.
  5. Requires strict private resolution for requests: a consumed Private Payment List is never reused, another endpoint from that list is not attempted, and public details are never used as fallback while waiting for a newer list.
  6. Updates Paykit to 0.1.0-rc44 and adds local E2E homeserver configuration plus safe cold-start restoration for externally managed Pubky sessions.

The request payload itself remains SDK-backed and durable; Bitkit persists only encrypted, identity-scoped presentation suppression, not a duplicate request queue. Payment proofs and receipts remain out of scope.

Dependencies:

Preview

N/A — proof recordings were completed locally and are intentionally not attached to the PR.

QA Notes

Manual Tests

  • 1. Clean wallet → create Pubky profile → enable Paykit and Contact Payments → add the peer as a contact: private-capable request action appears once the Noise link is established.
  • 2. Peer creates a private request → Home: Payment Request opens automatically with the correct contact and amount.
  • 3. Payment Request → dismiss without rejecting → bell → request preview → Pay: the request remains queued, reopens, and pays successfully.
  • 4. Peer creates a second request after payment → Pay: a newer Private Payment List is used; the consumed list is not reused and no public fallback occurs.
  • 5. Receive → Send Payment Request → enter amount, note, and expiry → select linked contact → Send: request is queued once and appears in sent history.
  • 6. Incoming request → Reject: only that request becomes terminal and the private payment list is not consumed.
  • 7. Incoming request → dismiss → relaunch: request remains discoverable without automatically reopening again; expired requests disappear live.
  • 8. Cross-platform E2E: iOS creates two requests that Android receives and pays, and Android creates two requests that iOS receives and pays, on clean regtest state.

Automated Checks

  • PaykitPaymentRequestRepoTest.kt: covers mapping, eligibility, proposal delivery, rejection, expiry, identity-scoped presentation state, and action serialization.
  • PaykitSdkServiceTest.kt: covers exact identity enforcement and safe deferred session restoration.
  • AppViewModelSendFlowTest.kt: covers automatic/manual presentation, sheet transitions, identity changes, newer-list retry, strict private resolution, and payment lifecycle races.
  • PaymentRequestExpirationTest.kt: covers expiry selection and retained draft state.
  • SheetHostTest.kt: covers locked sheet dismissal and scrim input isolation during durable proposal creation.
  • Local verification passed: app compile, Android-test compile, complete dev unit suite, detekt, and git diff --check.
  • Both final cross-platform proof videos decoded end to end without errors.

Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
Comment threadapp/src/main/java/to/bitkit/services/PaykitSdkService.kt Fixed
@greptile-apps

This comment has been minimized.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch 2 times, most recently from ba7f745 to e8183a5CompareAugust 20, 2026 10:41
ovitrif

This comment was marked as outdated.

@ovitrifovitrif added this to the 2.5.0 milestone Aug 20, 2026
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 5120677 to 7877465CompareAugust 21, 2026 01:16
@ovitrif

This comment was marked as outdated.

@piotr-iohk

This comment was marked as outdated.

@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from af0aaa4 to e2516e9CompareAugust 21, 2026 16:19
@ben-kaufmanben-kaufman mentioned this pull request Aug 24, 2026
2 tasks
@ovitrif
ovitrif self-requested a review August 24, 2026 16:21
ovitrif

This comment was marked as resolved.

jvsena42

This comment was marked as resolved.

ovitrif

This comment was marked as resolved.

ovitrif

This comment was marked as outdated.

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging while QA’ing with the staging public-pay pubkys — not a prod issue (Paykit UI is still hidden).

Enabling payments with contacts fail-closes with Unknown error if a saved contact’s receiver marker doesn’t parse (here: missing noise_public_key). I hit it on:

  • pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso
  • pubkynekaxdyt5ktoqyufd7cgug6qppa7999hm46wfqdzm6jqnr8tj16o

In principle the same thing could happen with any broken / outdated profile marker, not only these fixtures. Recording attached. Same surface on iOS after #676 (toast there is Private Paykit is not available).

Steps:

  1. Enable Paykit UI. Create a profile. Leave payments with contacts off.
  2. Add either (or both) staging pubkys above as contacts. They do not need to be live.
  3. Enable payments with contacts.
  4. Toast is Unknown error and the toggle stays off.
Screen.Recording.2026-08-26.at.10.12.34.mov

logs.zip

@piotr-iohk

piotr-iohk commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Flagging from QA: paying a payment request that’s larger than the wallet balance.

Same setup, two directions, both recordings attached:

  • iOS → Android (this PR): incoming sheet, Pay → toast Insufficient Spending Balance (e.g. ₿ 78,244 more needed to pay this Lightning invoice) and a red-bordered card. Dismiss then fails with Payment request operation is already in progress. Toast / error repeats; request stays on the sheet and is hard to dismiss.
Screen.Recording.2026-08-26.at.14.06.24.mov
  • Android → iOS (ldk-node Error #676, already merged):PayPlease try again. Coin selection failed. Dismiss is fine; the copy is just the wrong error.
Screen.Recording.2026-08-26.at.14.18.16.mov

Steps:

  1. Two live wallets with payments-with-contacts on. Put ~88k on the payer (split savings + spending is fine).
  2. Other wallet requests more than that total (100k Android-as-payer, 90k iOS-as-payer).
  3. On the incoming request, tap Pay, then try Dismiss.

Expected: don’t enter confirm if the amount is above spendable. On Android, Dismiss should still work after a failed Pay (no already in progress loop).

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed both QA findings in signed commit 5d39cd96a:

  • A malformed remote receiver marker is still rejected and cannot be used, but its per-contact inspection error no longer aborts global private endpoint publication for valid contacts. The regression test now covers the immediate-publication path used when enabling Contact Payments.
  • An unaffordable request now releases its manual presentation state after showing the existing insufficient-balance message. It remains pending and dismissible, is not accepted or rejected, and does not consume a Private Payment List. The regression test verifies Dismiss remains actionable and no retry starts from the old context.

The matching iOS fixes are isolated in synonymdev/bitkit-ios#684: malformed remote markers no longer block other contacts, and incoming request amounts are used during initial LN/on-chain balance validation so the standard insufficient Spending/Savings error appears before coin selection.

Local verification: 193 focused Android tests passed; detekt completed successfully with only unrelated pre-existing findings.

@piotr-iohk

piotr-iohk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Retested after 5d39cd9.

Enable — still fails. ❌

Toggle Enable payments with contactsUnknown error, toggle does not stay on. Same staging contact as before (pubky1jy146y751hkgqk9p69ds5cqf461aj5x7c8ptbca5rw6m9asotso). Recording + logs attached.

This is not the noise_public_key inspect-fail anymore. Logs are PrivateUnavailable. In this run I turned the toggle off first; disable cleanup already failed with that error, then re-enable with cleanup still pending fail-closes again. Settings still maps it to Unknown error.

Same user-facing miss on iOS #684 (toast there is Private Paykit is not available).

Steps:

  1. Paykit UI on. Profile exists. Staging pubky above saved as a contact (need not be live).
  2. Payments with contacts off (or turn it off first).
  3. Enable payments with contacts.
  4. Toast is Unknown error; toggle stays off.
Screen.Recording.2026-08-27.at.10.24.05.-.android.mov

logs-android.zip

Over-balance — looks good on Android. ✅

Incoming request larger than the wallet: one Insufficient Savings / Spending toast, request stays pending and dismissible. No already in progress loop.

iOS #684 still repeats that toast (see that PR).

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed git diff master...HEAD at 56ce990 (merge-base 6ae3cb7) — 34 files, ~3.8k added lines. 10 findings inline, roughly in severity order: the ReceiveSheet start-destination change and the dropped firstError in PrivatePaykitRepo look like the two most likely to bite in practice.

Checked and looked correct: the generation/identity guards in PaykitPaymentRequestRepo (activate/refresh/synchronizeLocked/isCurrentState), creationMutex.tryLock with finally unlock in propose, expiration rescheduling, the hideSheet re-entrancy guard, SheetHost's confirmValueChange + rememberUpdatedState pairing, the scrim's pointer-consuming modifier, the stopped/recursion loop in presentNextIncomingPaykitPaymentRequest, and the sats/BTC BigDecimal conversions.

One judgement call I did not flag as a defect: the string-literal match in canDeferStaleSession is brittle against SDK message changes.

Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt Outdated
Comment threadapp/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/components/Money.kt Outdated
Comment threadapp/src/main/res/values/strings.xml Outdated
@ben-kaufman
ben-kaufmanforce-pushed the codex/paykit-payment-request-ui-android branch from 56ce990 to 531578eCompareAugust 27, 2026 17:25
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Addressed the remaining enable/disable QA path in signed, rebased commit 531578e.

The private setting now reflects the user choice independently of transient transport cleanup or immediate Noise availability:

  • Disable commits private sharing off and records failed remote cleanup as durable deferred work instead of restoring the toggle.
  • Re-enable supersedes pending removal and prepares saved contacts opportunistically; it no longer requires a live peer or immediate private endpoint publication to keep the toggle enabled.
  • Recovery persists the cleanup marker before remote removal and local pruning, so a failure between those stages remains retryable. The cached-publication fallback still recovers older states where the marker was not stored.
  • Malformed remote markers remain unusable, and there is still no private-to-public payment fallback.

Regression coverage includes deferred disable cleanup, re-enable with pending cleanup, recovery without a marker, failure after remote cleanup but before local pruning, and total versus partial marker-selection failure. The over-balance behavior that passed the retest is unchanged.

Post-rebase verification: full testDevDebugUnitTest, detekt, and compileDevDebugAndroidTestKotlin pass. The stack is rebased onto current master and all 13 PR commits are signed.

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

Retested. LGTM.

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tAck

Reviewed the code manually and tested with journeys

@jvsena42
jvsena42 merged commit d644a1b into masterAug 28, 2026
18 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-payment-request-ui-android branch August 28, 2026 12:46
@ovitrifovitrif mentioned this pull request Aug 30, 2026
7 tasks
Sign up for freeto 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.

5 participants

@ben-kaufman@ovitrif@piotr-iohk@jvsena42@github-advanced-security