fix: secure shop payments and locked links - #1158

Merged
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae
Aug 15, 2026
Merged

fix: secure shop payments and locked links#1158
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Restricts native shop payment messages to the exact https://embed.bitrefill.com origin through an origin-scoped WebMessage listener.
  2. Disables native shop payments when origin-aware WebView messaging is unavailable, rejects non-string messages, and keeps broader Bitrefill navigation separate from the privileged payment origin.
  3. Defers payment links and scans until authentication completes while preserving the intentional QuickPay opt-in behavior.
  4. Keeps only the latest deferred external scan while incoming Paykit requests remain repository-backed until authentication.
  5. Serializes scan execution and preserves each scan's contact context so concurrent requests cannot overlap or be attributed to the wrong contact.

Addresses VulnHunter 01b finding 02 and the unauthenticated deeplink handling from finding 03. The explicit QuickPay bypass of per-payment PIN confirmation remains accepted product behavior.

Companion iOS PR: synonymdev/bitkit-ios#668

Preview

N/A — security boundary and payment-flow state handling only.

QA Notes

Manual Tests

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
  • 2a.regression: Shop → navigate between legitimate Bitrefill pages: navigation remains available.
    • 2b. Shop → attempt off-Bitrefill main-frame navigation: navigation is blocked.
  • 3. Settings → PIN on + PIN for payments on + QuickPay on → authenticate, then scan a small LN invoice: QuickPay opens without another PIN prompt, as designed.
  • 4. Settings → PIN on + PIN for payments off + QuickPay on → scan a small LN invoice: QuickPay still opens.
  • 5. Lock app → open a QuickPay-eligible lightning: link → authenticate: no payment flow opens before authentication, then QuickPay opens once.
  • 6. Lock app → open two different payment links → authenticate: only the latest link opens and the earlier link is not replayed.
  • 7. Lock app with an incoming Paykit request → open a payment link → authenticate → dismiss the external send flow: the external payment opens first, then the Paykit request opens once with its contact context.
  • 8. Lock app with a payment link deferred → open Scanner → authenticate → scan another invoice: the scanner result opens first and the deferred link opens after it is dismissed.

Automated Checks

  • ShopOriginTest.kt: covers the exact payment origin, trusted port/user-info boundary, broader navigation allow-list, and bridge-page guard.
  • ShopWebViewInterfaceTest.kt: covers feature detection, fail-closed fallback, exact listener origin, sibling-origin rejection, non-string messages, and payment parsing.
  • ShopWebViewClientTest.kt: covers supported/unsupported bridge injection, sibling-page non-injection, and main-frame navigation boundaries.
  • AppViewModelSendFlowTest.kt: covers authentication/QuickPay deferral, latest-wins replacement, context isolation, cancellation serialization, scanner ordering, sheet dismissal, and Paykit handoff races.
  • Test coverage removed: deleted FIFO/capacity/stale-queue cases because deferred external scans now use one latest-wins slot and automatic Paykit requests remain repository-backed.
  • Local verification: compileDevDebugKotlin, testDevDebugUnitTest, detekt --rerun-tasks, and git diff --check.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
@ben-kaufman
ben-kaufman marked this pull request as ready for review August 13, 2026 19:00
@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR restricts the shop WebView to trusted Bitrefill HTTPS origins, makes QuickPay respect payment PIN settings, and defers payment inputs until authentication. It also adds focused tests and a security changelog entry.

  • Adds shared Bitrefill host/origin validation for navigation and WebView messages.
  • Prevents QuickPay from bypassing payment PIN confirmation.
  • Defers locked scans and deeplinks, although multiple pending inputs currently overwrite one another.
  • Adds unit coverage for origin filtering, navigation blocking, PIN-aware QuickPay, and single pending-input replay.

Confidence Score: 4/5

The PR should not merge until locked payment inputs can no longer silently overwrite earlier pending requests.

The new authentication gate retains only one pending scan or deeplink, so consecutive Android payment intents received before unlock cause earlier requests to be discarded.

Files Needing Attention: app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.ktIntroduces HTTPS Bitrefill host validation and an origin-filtering JavaScript message bridge.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewClient.ktInjects the restricted bridge and blocks untrusted main-frame navigation.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewInterface.ktMoves bridge handling onto the main thread and validates the current page origin before forwarding payment intents.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewScreen.ktSupplies the current WebView URL to the interface for origin validation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.ktAdds PIN-aware QuickPay suppression and locked-input deferral, but the single pending slot silently drops all but the latest input.
app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.ktCovers replay of one locked input and PIN-aware QuickPay but does not exercise multiple inputs received before unlock.

Sequence Diagram

sequenceDiagram
participant OS as Android intents
participant VM as AppViewModel
participant Lock as Authentication UI
participant Send as Send flow
OS->>VM: Payment input A
VM->>VM: "pendingLockedScan = A"
OS->>VM: Payment input B
VM->>VM: "pendingLockedScan = B"
Note over VM: Input A is overwritten
Lock->>VM: setIsAuthenticated(true)
VM->>VM: flushPendingLockedScan()
VM->>Send: Launch input B only
Loading

Reviews (1): Last reviewed commit: "fix: handle null shop origin scheme" | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated

@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 + manual verification on a regtest dev build (Pixel_9 emulator).

Verified working on device: QuickPay is skipped when PIN-for-payments is on (Skipping QuickPay because PIN is required for payments → Confirm sheet → PIN prompt on swipe); QuickPay still runs with PIN-for-payments off; payment links fired while locked are queued and opened exactly once after auth; a second link replaces the first (Replacing deferred scan) and the earlier one is never replayed; off-Bitrefill main-frame navigation is blocked (Blocked shop navigation to untrusted origin 'https://policies.google.com/privacy') while in-shop navigation is unaffected.

Not covered on device: Bitrefill checkout (dev flavor talks to the real embed.bitrefill.com, so a checkout emits a mainnet invoice a regtest wallet can't act on), the Paykit handoff race, and the scanner-ordering case.

5 inline findings below.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.kt Outdated
@pwltr

pwltr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note that QuickPay skipping PIN checks is assumed risk per design.

image

@ben-kaufmanben-kaufman changed the title fix: gate shop origin and pin payfix: secure shop payments and locked linksAug 14, 2026
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr Thanks for clarifying. Agreed—QuickPay is the explicit opt-in exception to per-payment PIN confirmation. I removed the new PIN-for-payments gate in 32b59a5, kept the app-authentication deferral for locked links, and updated the regression test, changelog, title, and PR description to state the accepted behavior accurately. The focused test, full unit suite, compile, and detekt checks are green.

@ovitrifovitrif mentioned this pull request Aug 15, 2026
10 tasks
@ovitrif

This comment was marked as outdated.

@ovitrif
ovitrif self-requested a review August 15, 2026 12:58
cursoragentand others added 12 commits August 15, 2026 20:27
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ovitrif
ovitrifforce-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192CompareAugust 15, 2026 18:28
@ovitrif
ovitrif requested a review from jvsena42August 15, 2026 18:34
@ovitrifovitrif added this to the 2.5.0 milestone Aug 15, 2026

@ovitrifovitrif 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.

The shop payment origin gate, fail-closed WebView messaging, and locked-link deferral look correct on 49ab192, including the latest-wins deferred scan and the accepted QuickPay PIN bypass.

@ovitrif
ovitrif merged commit 5ccc5c0 into masterAug 15, 2026
18 checks passed
@ovitrif
ovitrif deleted the cursor/fix-shop-quickpay-auth-86ae branch August 15, 2026 23:03
@ovitrif

Copy link
Copy Markdown
Collaborator

Emulator QA on to.bitkit.dev @ 5ccc5c038 (Pixel emulator-5554). PIN 1234, QuickPay on, spending 400000 sats. Self-pay of this wallet's invoices fails after QuickPay starts (DuplicatePayment); the check was that Confirm/QuickPay opened, not that the payment settled.

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
    Reached Zalando €20 cart. Did not submit. Dev shop talks to real embed.bitrefill.com (mainnet invoice on a regtest wallet).
  • 2a. Shop → navigate between legitimate Bitrefill pages: Gift Cards → eSIMs stayed on Bitrefill.
  • 2b. Shop → off-Bitrefill main-frame nav: https://evil.example/pay blocked. Toast: "This link can’t be opened from the shop."
  • 3. PIN on + PIN for payments on + QuickPay on → small LN invoice: QuickPay opened with no second PIN (500 sats).
  • 4. PIN on + PIN for payments off + QuickPay on → small LN invoice: QuickPay still opened (600 sats).
  • 5. Lock app → QuickPay-eligible lightning: link → authenticate: no send UI before PIN, then QuickPay once (700 sats).
  • 6. Lock app → two payment links → authenticate: 800 sat replaced, only 900 sat opened.
  • 7. Lock + incoming Paykit request + payment link: not run. Android has no proposePaymentRequest UI. Mutual contacts only enable Contact Pay (outgoing). Incoming requests come from Paykit Server / Locks (paykit-payment invoice), not Bitkit ↔ Bitkit.
  • 8. Lock + deferred lightning: link → open Scanner → authenticate → scan another invoice: scanner result opened first, deferred 400-sat QuickPay opened after dismiss.

Local artifacts: .ai/pr/1158/review/

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.

6 participants

@ben-kaufman@pwltr@ovitrif@jvsena42@github-advanced-security@cursoragent
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} 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

fix: secure shop payments and locked links - #1158

Merged
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae
Aug 15, 2026
Merged

fix: secure shop payments and locked links#1158
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Restricts native shop payment messages to the exact https://embed.bitrefill.com origin through an origin-scoped WebMessage listener.
  2. Disables native shop payments when origin-aware WebView messaging is unavailable, rejects non-string messages, and keeps broader Bitrefill navigation separate from the privileged payment origin.
  3. Defers payment links and scans until authentication completes while preserving the intentional QuickPay opt-in behavior.
  4. Keeps only the latest deferred external scan while incoming Paykit requests remain repository-backed until authentication.
  5. Serializes scan execution and preserves each scan's contact context so concurrent requests cannot overlap or be attributed to the wrong contact.

Addresses VulnHunter 01b finding 02 and the unauthenticated deeplink handling from finding 03. The explicit QuickPay bypass of per-payment PIN confirmation remains accepted product behavior.

Companion iOS PR: synonymdev/bitkit-ios#668

Preview

N/A — security boundary and payment-flow state handling only.

QA Notes

Manual Tests

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
  • 2a.regression: Shop → navigate between legitimate Bitrefill pages: navigation remains available.
    • 2b. Shop → attempt off-Bitrefill main-frame navigation: navigation is blocked.
  • 3. Settings → PIN on + PIN for payments on + QuickPay on → authenticate, then scan a small LN invoice: QuickPay opens without another PIN prompt, as designed.
  • 4. Settings → PIN on + PIN for payments off + QuickPay on → scan a small LN invoice: QuickPay still opens.
  • 5. Lock app → open a QuickPay-eligible lightning: link → authenticate: no payment flow opens before authentication, then QuickPay opens once.
  • 6. Lock app → open two different payment links → authenticate: only the latest link opens and the earlier link is not replayed.
  • 7. Lock app with an incoming Paykit request → open a payment link → authenticate → dismiss the external send flow: the external payment opens first, then the Paykit request opens once with its contact context.
  • 8. Lock app with a payment link deferred → open Scanner → authenticate → scan another invoice: the scanner result opens first and the deferred link opens after it is dismissed.

Automated Checks

  • ShopOriginTest.kt: covers the exact payment origin, trusted port/user-info boundary, broader navigation allow-list, and bridge-page guard.
  • ShopWebViewInterfaceTest.kt: covers feature detection, fail-closed fallback, exact listener origin, sibling-origin rejection, non-string messages, and payment parsing.
  • ShopWebViewClientTest.kt: covers supported/unsupported bridge injection, sibling-page non-injection, and main-frame navigation boundaries.
  • AppViewModelSendFlowTest.kt: covers authentication/QuickPay deferral, latest-wins replacement, context isolation, cancellation serialization, scanner ordering, sheet dismissal, and Paykit handoff races.
  • Test coverage removed: deleted FIFO/capacity/stale-queue cases because deferred external scans now use one latest-wins slot and automatic Paykit requests remain repository-backed.
  • Local verification: compileDevDebugKotlin, testDevDebugUnitTest, detekt --rerun-tasks, and git diff --check.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
@ben-kaufman
ben-kaufman marked this pull request as ready for review August 13, 2026 19:00
@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR restricts the shop WebView to trusted Bitrefill HTTPS origins, makes QuickPay respect payment PIN settings, and defers payment inputs until authentication. It also adds focused tests and a security changelog entry.

  • Adds shared Bitrefill host/origin validation for navigation and WebView messages.
  • Prevents QuickPay from bypassing payment PIN confirmation.
  • Defers locked scans and deeplinks, although multiple pending inputs currently overwrite one another.
  • Adds unit coverage for origin filtering, navigation blocking, PIN-aware QuickPay, and single pending-input replay.

Confidence Score: 4/5

The PR should not merge until locked payment inputs can no longer silently overwrite earlier pending requests.

The new authentication gate retains only one pending scan or deeplink, so consecutive Android payment intents received before unlock cause earlier requests to be discarded.

Files Needing Attention: app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.ktIntroduces HTTPS Bitrefill host validation and an origin-filtering JavaScript message bridge.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewClient.ktInjects the restricted bridge and blocks untrusted main-frame navigation.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewInterface.ktMoves bridge handling onto the main thread and validates the current page origin before forwarding payment intents.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewScreen.ktSupplies the current WebView URL to the interface for origin validation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.ktAdds PIN-aware QuickPay suppression and locked-input deferral, but the single pending slot silently drops all but the latest input.
app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.ktCovers replay of one locked input and PIN-aware QuickPay but does not exercise multiple inputs received before unlock.

Sequence Diagram

sequenceDiagram
participant OS as Android intents
participant VM as AppViewModel
participant Lock as Authentication UI
participant Send as Send flow
OS->>VM: Payment input A
VM->>VM: "pendingLockedScan = A"
OS->>VM: Payment input B
VM->>VM: "pendingLockedScan = B"
Note over VM: Input A is overwritten
Lock->>VM: setIsAuthenticated(true)
VM->>VM: flushPendingLockedScan()
VM->>Send: Launch input B only
Loading

Reviews (1): Last reviewed commit: "fix: handle null shop origin scheme" | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated

@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 + manual verification on a regtest dev build (Pixel_9 emulator).

Verified working on device: QuickPay is skipped when PIN-for-payments is on (Skipping QuickPay because PIN is required for payments → Confirm sheet → PIN prompt on swipe); QuickPay still runs with PIN-for-payments off; payment links fired while locked are queued and opened exactly once after auth; a second link replaces the first (Replacing deferred scan) and the earlier one is never replayed; off-Bitrefill main-frame navigation is blocked (Blocked shop navigation to untrusted origin 'https://policies.google.com/privacy') while in-shop navigation is unaffected.

Not covered on device: Bitrefill checkout (dev flavor talks to the real embed.bitrefill.com, so a checkout emits a mainnet invoice a regtest wallet can't act on), the Paykit handoff race, and the scanner-ordering case.

5 inline findings below.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.kt Outdated
@pwltr

pwltr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note that QuickPay skipping PIN checks is assumed risk per design.

image

@ben-kaufmanben-kaufman changed the title fix: gate shop origin and pin payfix: secure shop payments and locked linksAug 14, 2026
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr Thanks for clarifying. Agreed—QuickPay is the explicit opt-in exception to per-payment PIN confirmation. I removed the new PIN-for-payments gate in 32b59a5, kept the app-authentication deferral for locked links, and updated the regression test, changelog, title, and PR description to state the accepted behavior accurately. The focused test, full unit suite, compile, and detekt checks are green.

@ovitrifovitrif mentioned this pull request Aug 15, 2026
10 tasks
@ovitrif

This comment was marked as outdated.

@ovitrif
ovitrif self-requested a review August 15, 2026 12:58
cursoragentand others added 12 commits August 15, 2026 20:27
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ovitrif
ovitrifforce-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192CompareAugust 15, 2026 18:28
@ovitrif
ovitrif requested a review from jvsena42August 15, 2026 18:34
@ovitrifovitrif added this to the 2.5.0 milestone Aug 15, 2026

@ovitrifovitrif 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.

The shop payment origin gate, fail-closed WebView messaging, and locked-link deferral look correct on 49ab192, including the latest-wins deferred scan and the accepted QuickPay PIN bypass.

@ovitrif
ovitrif merged commit 5ccc5c0 into masterAug 15, 2026
18 checks passed
@ovitrif
ovitrif deleted the cursor/fix-shop-quickpay-auth-86ae branch August 15, 2026 23:03
@ovitrif

Copy link
Copy Markdown
Collaborator

Emulator QA on to.bitkit.dev @ 5ccc5c038 (Pixel emulator-5554). PIN 1234, QuickPay on, spending 400000 sats. Self-pay of this wallet's invoices fails after QuickPay starts (DuplicatePayment); the check was that Confirm/QuickPay opened, not that the payment settled.

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
    Reached Zalando €20 cart. Did not submit. Dev shop talks to real embed.bitrefill.com (mainnet invoice on a regtest wallet).
  • 2a. Shop → navigate between legitimate Bitrefill pages: Gift Cards → eSIMs stayed on Bitrefill.
  • 2b. Shop → off-Bitrefill main-frame nav: https://evil.example/pay blocked. Toast: "This link can’t be opened from the shop."
  • 3. PIN on + PIN for payments on + QuickPay on → small LN invoice: QuickPay opened with no second PIN (500 sats).
  • 4. PIN on + PIN for payments off + QuickPay on → small LN invoice: QuickPay still opened (600 sats).
  • 5. Lock app → QuickPay-eligible lightning: link → authenticate: no send UI before PIN, then QuickPay once (700 sats).
  • 6. Lock app → two payment links → authenticate: 800 sat replaced, only 900 sat opened.
  • 7. Lock + incoming Paykit request + payment link: not run. Android has no proposePaymentRequest UI. Mutual contacts only enable Contact Pay (outgoing). Incoming requests come from Paykit Server / Locks (paykit-payment invoice), not Bitkit ↔ Bitkit.
  • 8. Lock + deferred lightning: link → open Scanner → authenticate → scan another invoice: scanner result opened first, deferred 400-sat QuickPay opened after dismiss.

Local artifacts: .ai/pr/1158/review/

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.

6 participants

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

fix: secure shop payments and locked links - #1158

Merged
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae
Aug 15, 2026
Merged

fix: secure shop payments and locked links#1158
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Restricts native shop payment messages to the exact https://embed.bitrefill.com origin through an origin-scoped WebMessage listener.
  2. Disables native shop payments when origin-aware WebView messaging is unavailable, rejects non-string messages, and keeps broader Bitrefill navigation separate from the privileged payment origin.
  3. Defers payment links and scans until authentication completes while preserving the intentional QuickPay opt-in behavior.
  4. Keeps only the latest deferred external scan while incoming Paykit requests remain repository-backed until authentication.
  5. Serializes scan execution and preserves each scan's contact context so concurrent requests cannot overlap or be attributed to the wrong contact.

Addresses VulnHunter 01b finding 02 and the unauthenticated deeplink handling from finding 03. The explicit QuickPay bypass of per-payment PIN confirmation remains accepted product behavior.

Companion iOS PR: synonymdev/bitkit-ios#668

Preview

N/A — security boundary and payment-flow state handling only.

QA Notes

Manual Tests

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
  • 2a.regression: Shop → navigate between legitimate Bitrefill pages: navigation remains available.
    • 2b. Shop → attempt off-Bitrefill main-frame navigation: navigation is blocked.
  • 3. Settings → PIN on + PIN for payments on + QuickPay on → authenticate, then scan a small LN invoice: QuickPay opens without another PIN prompt, as designed.
  • 4. Settings → PIN on + PIN for payments off + QuickPay on → scan a small LN invoice: QuickPay still opens.
  • 5. Lock app → open a QuickPay-eligible lightning: link → authenticate: no payment flow opens before authentication, then QuickPay opens once.
  • 6. Lock app → open two different payment links → authenticate: only the latest link opens and the earlier link is not replayed.
  • 7. Lock app with an incoming Paykit request → open a payment link → authenticate → dismiss the external send flow: the external payment opens first, then the Paykit request opens once with its contact context.
  • 8. Lock app with a payment link deferred → open Scanner → authenticate → scan another invoice: the scanner result opens first and the deferred link opens after it is dismissed.

Automated Checks

  • ShopOriginTest.kt: covers the exact payment origin, trusted port/user-info boundary, broader navigation allow-list, and bridge-page guard.
  • ShopWebViewInterfaceTest.kt: covers feature detection, fail-closed fallback, exact listener origin, sibling-origin rejection, non-string messages, and payment parsing.
  • ShopWebViewClientTest.kt: covers supported/unsupported bridge injection, sibling-page non-injection, and main-frame navigation boundaries.
  • AppViewModelSendFlowTest.kt: covers authentication/QuickPay deferral, latest-wins replacement, context isolation, cancellation serialization, scanner ordering, sheet dismissal, and Paykit handoff races.
  • Test coverage removed: deleted FIFO/capacity/stale-queue cases because deferred external scans now use one latest-wins slot and automatic Paykit requests remain repository-backed.
  • Local verification: compileDevDebugKotlin, testDevDebugUnitTest, detekt --rerun-tasks, and git diff --check.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
@ben-kaufman
ben-kaufman marked this pull request as ready for review August 13, 2026 19:00
@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR restricts the shop WebView to trusted Bitrefill HTTPS origins, makes QuickPay respect payment PIN settings, and defers payment inputs until authentication. It also adds focused tests and a security changelog entry.

  • Adds shared Bitrefill host/origin validation for navigation and WebView messages.
  • Prevents QuickPay from bypassing payment PIN confirmation.
  • Defers locked scans and deeplinks, although multiple pending inputs currently overwrite one another.
  • Adds unit coverage for origin filtering, navigation blocking, PIN-aware QuickPay, and single pending-input replay.

Confidence Score: 4/5

The PR should not merge until locked payment inputs can no longer silently overwrite earlier pending requests.

The new authentication gate retains only one pending scan or deeplink, so consecutive Android payment intents received before unlock cause earlier requests to be discarded.

Files Needing Attention: app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.ktIntroduces HTTPS Bitrefill host validation and an origin-filtering JavaScript message bridge.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewClient.ktInjects the restricted bridge and blocks untrusted main-frame navigation.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewInterface.ktMoves bridge handling onto the main thread and validates the current page origin before forwarding payment intents.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewScreen.ktSupplies the current WebView URL to the interface for origin validation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.ktAdds PIN-aware QuickPay suppression and locked-input deferral, but the single pending slot silently drops all but the latest input.
app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.ktCovers replay of one locked input and PIN-aware QuickPay but does not exercise multiple inputs received before unlock.

Sequence Diagram

sequenceDiagram
participant OS as Android intents
participant VM as AppViewModel
participant Lock as Authentication UI
participant Send as Send flow
OS->>VM: Payment input A
VM->>VM: "pendingLockedScan = A"
OS->>VM: Payment input B
VM->>VM: "pendingLockedScan = B"
Note over VM: Input A is overwritten
Lock->>VM: setIsAuthenticated(true)
VM->>VM: flushPendingLockedScan()
VM->>Send: Launch input B only
Loading

Reviews (1): Last reviewed commit: "fix: handle null shop origin scheme" | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated

@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 + manual verification on a regtest dev build (Pixel_9 emulator).

Verified working on device: QuickPay is skipped when PIN-for-payments is on (Skipping QuickPay because PIN is required for payments → Confirm sheet → PIN prompt on swipe); QuickPay still runs with PIN-for-payments off; payment links fired while locked are queued and opened exactly once after auth; a second link replaces the first (Replacing deferred scan) and the earlier one is never replayed; off-Bitrefill main-frame navigation is blocked (Blocked shop navigation to untrusted origin 'https://policies.google.com/privacy') while in-shop navigation is unaffected.

Not covered on device: Bitrefill checkout (dev flavor talks to the real embed.bitrefill.com, so a checkout emits a mainnet invoice a regtest wallet can't act on), the Paykit handoff race, and the scanner-ordering case.

5 inline findings below.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.kt Outdated
@pwltr

pwltr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note that QuickPay skipping PIN checks is assumed risk per design.

image

@ben-kaufmanben-kaufman changed the title fix: gate shop origin and pin payfix: secure shop payments and locked linksAug 14, 2026
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr Thanks for clarifying. Agreed—QuickPay is the explicit opt-in exception to per-payment PIN confirmation. I removed the new PIN-for-payments gate in 32b59a5, kept the app-authentication deferral for locked links, and updated the regression test, changelog, title, and PR description to state the accepted behavior accurately. The focused test, full unit suite, compile, and detekt checks are green.

@ovitrifovitrif mentioned this pull request Aug 15, 2026
10 tasks
@ovitrif

This comment was marked as outdated.

@ovitrif
ovitrif self-requested a review August 15, 2026 12:58
cursoragentand others added 12 commits August 15, 2026 20:27
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ovitrif
ovitrifforce-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192CompareAugust 15, 2026 18:28
@ovitrif
ovitrif requested a review from jvsena42August 15, 2026 18:34
@ovitrifovitrif added this to the 2.5.0 milestone Aug 15, 2026

@ovitrifovitrif 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.

The shop payment origin gate, fail-closed WebView messaging, and locked-link deferral look correct on 49ab192, including the latest-wins deferred scan and the accepted QuickPay PIN bypass.

@ovitrif
ovitrif merged commit 5ccc5c0 into masterAug 15, 2026
18 checks passed
@ovitrif
ovitrif deleted the cursor/fix-shop-quickpay-auth-86ae branch August 15, 2026 23:03
@ovitrif

Copy link
Copy Markdown
Collaborator

Emulator QA on to.bitkit.dev @ 5ccc5c038 (Pixel emulator-5554). PIN 1234, QuickPay on, spending 400000 sats. Self-pay of this wallet's invoices fails after QuickPay starts (DuplicatePayment); the check was that Confirm/QuickPay opened, not that the payment settled.

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
    Reached Zalando €20 cart. Did not submit. Dev shop talks to real embed.bitrefill.com (mainnet invoice on a regtest wallet).
  • 2a. Shop → navigate between legitimate Bitrefill pages: Gift Cards → eSIMs stayed on Bitrefill.
  • 2b. Shop → off-Bitrefill main-frame nav: https://evil.example/pay blocked. Toast: "This link can’t be opened from the shop."
  • 3. PIN on + PIN for payments on + QuickPay on → small LN invoice: QuickPay opened with no second PIN (500 sats).
  • 4. PIN on + PIN for payments off + QuickPay on → small LN invoice: QuickPay still opened (600 sats).
  • 5. Lock app → QuickPay-eligible lightning: link → authenticate: no send UI before PIN, then QuickPay once (700 sats).
  • 6. Lock app → two payment links → authenticate: 800 sat replaced, only 900 sat opened.
  • 7. Lock + incoming Paykit request + payment link: not run. Android has no proposePaymentRequest UI. Mutual contacts only enable Contact Pay (outgoing). Incoming requests come from Paykit Server / Locks (paykit-payment invoice), not Bitkit ↔ Bitkit.
  • 8. Lock + deferred lightning: link → open Scanner → authenticate → scan another invoice: scanner result opened first, deferred 400-sat QuickPay opened after dismiss.

Local artifacts: .ai/pr/1158/review/

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.

6 participants

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

fix: secure shop payments and locked links - #1158

Merged
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae
Aug 15, 2026
Merged

fix: secure shop payments and locked links#1158
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Restricts native shop payment messages to the exact https://embed.bitrefill.com origin through an origin-scoped WebMessage listener.
  2. Disables native shop payments when origin-aware WebView messaging is unavailable, rejects non-string messages, and keeps broader Bitrefill navigation separate from the privileged payment origin.
  3. Defers payment links and scans until authentication completes while preserving the intentional QuickPay opt-in behavior.
  4. Keeps only the latest deferred external scan while incoming Paykit requests remain repository-backed until authentication.
  5. Serializes scan execution and preserves each scan's contact context so concurrent requests cannot overlap or be attributed to the wrong contact.

Addresses VulnHunter 01b finding 02 and the unauthenticated deeplink handling from finding 03. The explicit QuickPay bypass of per-payment PIN confirmation remains accepted product behavior.

Companion iOS PR: synonymdev/bitkit-ios#668

Preview

N/A — security boundary and payment-flow state handling only.

QA Notes

Manual Tests

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
  • 2a.regression: Shop → navigate between legitimate Bitrefill pages: navigation remains available.
    • 2b. Shop → attempt off-Bitrefill main-frame navigation: navigation is blocked.
  • 3. Settings → PIN on + PIN for payments on + QuickPay on → authenticate, then scan a small LN invoice: QuickPay opens without another PIN prompt, as designed.
  • 4. Settings → PIN on + PIN for payments off + QuickPay on → scan a small LN invoice: QuickPay still opens.
  • 5. Lock app → open a QuickPay-eligible lightning: link → authenticate: no payment flow opens before authentication, then QuickPay opens once.
  • 6. Lock app → open two different payment links → authenticate: only the latest link opens and the earlier link is not replayed.
  • 7. Lock app with an incoming Paykit request → open a payment link → authenticate → dismiss the external send flow: the external payment opens first, then the Paykit request opens once with its contact context.
  • 8. Lock app with a payment link deferred → open Scanner → authenticate → scan another invoice: the scanner result opens first and the deferred link opens after it is dismissed.

Automated Checks

  • ShopOriginTest.kt: covers the exact payment origin, trusted port/user-info boundary, broader navigation allow-list, and bridge-page guard.
  • ShopWebViewInterfaceTest.kt: covers feature detection, fail-closed fallback, exact listener origin, sibling-origin rejection, non-string messages, and payment parsing.
  • ShopWebViewClientTest.kt: covers supported/unsupported bridge injection, sibling-page non-injection, and main-frame navigation boundaries.
  • AppViewModelSendFlowTest.kt: covers authentication/QuickPay deferral, latest-wins replacement, context isolation, cancellation serialization, scanner ordering, sheet dismissal, and Paykit handoff races.
  • Test coverage removed: deleted FIFO/capacity/stale-queue cases because deferred external scans now use one latest-wins slot and automatic Paykit requests remain repository-backed.
  • Local verification: compileDevDebugKotlin, testDevDebugUnitTest, detekt --rerun-tasks, and git diff --check.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
@ben-kaufman
ben-kaufman marked this pull request as ready for review August 13, 2026 19:00
@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR restricts the shop WebView to trusted Bitrefill HTTPS origins, makes QuickPay respect payment PIN settings, and defers payment inputs until authentication. It also adds focused tests and a security changelog entry.

  • Adds shared Bitrefill host/origin validation for navigation and WebView messages.
  • Prevents QuickPay from bypassing payment PIN confirmation.
  • Defers locked scans and deeplinks, although multiple pending inputs currently overwrite one another.
  • Adds unit coverage for origin filtering, navigation blocking, PIN-aware QuickPay, and single pending-input replay.

Confidence Score: 4/5

The PR should not merge until locked payment inputs can no longer silently overwrite earlier pending requests.

The new authentication gate retains only one pending scan or deeplink, so consecutive Android payment intents received before unlock cause earlier requests to be discarded.

Files Needing Attention: app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.ktIntroduces HTTPS Bitrefill host validation and an origin-filtering JavaScript message bridge.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewClient.ktInjects the restricted bridge and blocks untrusted main-frame navigation.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewInterface.ktMoves bridge handling onto the main thread and validates the current page origin before forwarding payment intents.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewScreen.ktSupplies the current WebView URL to the interface for origin validation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.ktAdds PIN-aware QuickPay suppression and locked-input deferral, but the single pending slot silently drops all but the latest input.
app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.ktCovers replay of one locked input and PIN-aware QuickPay but does not exercise multiple inputs received before unlock.

Sequence Diagram

sequenceDiagram
participant OS as Android intents
participant VM as AppViewModel
participant Lock as Authentication UI
participant Send as Send flow
OS->>VM: Payment input A
VM->>VM: "pendingLockedScan = A"
OS->>VM: Payment input B
VM->>VM: "pendingLockedScan = B"
Note over VM: Input A is overwritten
Lock->>VM: setIsAuthenticated(true)
VM->>VM: flushPendingLockedScan()
VM->>Send: Launch input B only
Loading

Reviews (1): Last reviewed commit: "fix: handle null shop origin scheme" | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated

@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 + manual verification on a regtest dev build (Pixel_9 emulator).

Verified working on device: QuickPay is skipped when PIN-for-payments is on (Skipping QuickPay because PIN is required for payments → Confirm sheet → PIN prompt on swipe); QuickPay still runs with PIN-for-payments off; payment links fired while locked are queued and opened exactly once after auth; a second link replaces the first (Replacing deferred scan) and the earlier one is never replayed; off-Bitrefill main-frame navigation is blocked (Blocked shop navigation to untrusted origin 'https://policies.google.com/privacy') while in-shop navigation is unaffected.

Not covered on device: Bitrefill checkout (dev flavor talks to the real embed.bitrefill.com, so a checkout emits a mainnet invoice a regtest wallet can't act on), the Paykit handoff race, and the scanner-ordering case.

5 inline findings below.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.kt Outdated
@pwltr

pwltr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note that QuickPay skipping PIN checks is assumed risk per design.

image

@ben-kaufmanben-kaufman changed the title fix: gate shop origin and pin payfix: secure shop payments and locked linksAug 14, 2026
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr Thanks for clarifying. Agreed—QuickPay is the explicit opt-in exception to per-payment PIN confirmation. I removed the new PIN-for-payments gate in 32b59a5, kept the app-authentication deferral for locked links, and updated the regression test, changelog, title, and PR description to state the accepted behavior accurately. The focused test, full unit suite, compile, and detekt checks are green.

@ovitrifovitrif mentioned this pull request Aug 15, 2026
10 tasks
@ovitrif

This comment was marked as outdated.

@ovitrif
ovitrif self-requested a review August 15, 2026 12:58
cursoragentand others added 12 commits August 15, 2026 20:27
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ovitrif
ovitrifforce-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192CompareAugust 15, 2026 18:28
@ovitrif
ovitrif requested a review from jvsena42August 15, 2026 18:34
@ovitrifovitrif added this to the 2.5.0 milestone Aug 15, 2026

@ovitrifovitrif 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.

The shop payment origin gate, fail-closed WebView messaging, and locked-link deferral look correct on 49ab192, including the latest-wins deferred scan and the accepted QuickPay PIN bypass.

@ovitrif
ovitrif merged commit 5ccc5c0 into masterAug 15, 2026
18 checks passed
@ovitrif
ovitrif deleted the cursor/fix-shop-quickpay-auth-86ae branch August 15, 2026 23:03
@ovitrif

Copy link
Copy Markdown
Collaborator

Emulator QA on to.bitkit.dev @ 5ccc5c038 (Pixel emulator-5554). PIN 1234, QuickPay on, spending 400000 sats. Self-pay of this wallet's invoices fails after QuickPay starts (DuplicatePayment); the check was that Confirm/QuickPay opened, not that the payment settled.

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
    Reached Zalando €20 cart. Did not submit. Dev shop talks to real embed.bitrefill.com (mainnet invoice on a regtest wallet).
  • 2a. Shop → navigate between legitimate Bitrefill pages: Gift Cards → eSIMs stayed on Bitrefill.
  • 2b. Shop → off-Bitrefill main-frame nav: https://evil.example/pay blocked. Toast: "This link can’t be opened from the shop."
  • 3. PIN on + PIN for payments on + QuickPay on → small LN invoice: QuickPay opened with no second PIN (500 sats).
  • 4. PIN on + PIN for payments off + QuickPay on → small LN invoice: QuickPay still opened (600 sats).
  • 5. Lock app → QuickPay-eligible lightning: link → authenticate: no send UI before PIN, then QuickPay once (700 sats).
  • 6. Lock app → two payment links → authenticate: 800 sat replaced, only 900 sat opened.
  • 7. Lock + incoming Paykit request + payment link: not run. Android has no proposePaymentRequest UI. Mutual contacts only enable Contact Pay (outgoing). Incoming requests come from Paykit Server / Locks (paykit-payment invoice), not Bitkit ↔ Bitkit.
  • 8. Lock + deferred lightning: link → open Scanner → authenticate → scan another invoice: scanner result opened first, deferred 400-sat QuickPay opened after dismiss.

Local artifacts: .ai/pr/1158/review/

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.

6 participants

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

fix: secure shop payments and locked links - #1158

Merged
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae
Aug 15, 2026
Merged

fix: secure shop payments and locked links#1158
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Restricts native shop payment messages to the exact https://embed.bitrefill.com origin through an origin-scoped WebMessage listener.
  2. Disables native shop payments when origin-aware WebView messaging is unavailable, rejects non-string messages, and keeps broader Bitrefill navigation separate from the privileged payment origin.
  3. Defers payment links and scans until authentication completes while preserving the intentional QuickPay opt-in behavior.
  4. Keeps only the latest deferred external scan while incoming Paykit requests remain repository-backed until authentication.
  5. Serializes scan execution and preserves each scan's contact context so concurrent requests cannot overlap or be attributed to the wrong contact.

Addresses VulnHunter 01b finding 02 and the unauthenticated deeplink handling from finding 03. The explicit QuickPay bypass of per-payment PIN confirmation remains accepted product behavior.

Companion iOS PR: synonymdev/bitkit-ios#668

Preview

N/A — security boundary and payment-flow state handling only.

QA Notes

Manual Tests

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
  • 2a.regression: Shop → navigate between legitimate Bitrefill pages: navigation remains available.
    • 2b. Shop → attempt off-Bitrefill main-frame navigation: navigation is blocked.
  • 3. Settings → PIN on + PIN for payments on + QuickPay on → authenticate, then scan a small LN invoice: QuickPay opens without another PIN prompt, as designed.
  • 4. Settings → PIN on + PIN for payments off + QuickPay on → scan a small LN invoice: QuickPay still opens.
  • 5. Lock app → open a QuickPay-eligible lightning: link → authenticate: no payment flow opens before authentication, then QuickPay opens once.
  • 6. Lock app → open two different payment links → authenticate: only the latest link opens and the earlier link is not replayed.
  • 7. Lock app with an incoming Paykit request → open a payment link → authenticate → dismiss the external send flow: the external payment opens first, then the Paykit request opens once with its contact context.
  • 8. Lock app with a payment link deferred → open Scanner → authenticate → scan another invoice: the scanner result opens first and the deferred link opens after it is dismissed.

Automated Checks

  • ShopOriginTest.kt: covers the exact payment origin, trusted port/user-info boundary, broader navigation allow-list, and bridge-page guard.
  • ShopWebViewInterfaceTest.kt: covers feature detection, fail-closed fallback, exact listener origin, sibling-origin rejection, non-string messages, and payment parsing.
  • ShopWebViewClientTest.kt: covers supported/unsupported bridge injection, sibling-page non-injection, and main-frame navigation boundaries.
  • AppViewModelSendFlowTest.kt: covers authentication/QuickPay deferral, latest-wins replacement, context isolation, cancellation serialization, scanner ordering, sheet dismissal, and Paykit handoff races.
  • Test coverage removed: deleted FIFO/capacity/stale-queue cases because deferred external scans now use one latest-wins slot and automatic Paykit requests remain repository-backed.
  • Local verification: compileDevDebugKotlin, testDevDebugUnitTest, detekt --rerun-tasks, and git diff --check.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
@ben-kaufman
ben-kaufman marked this pull request as ready for review August 13, 2026 19:00
@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR restricts the shop WebView to trusted Bitrefill HTTPS origins, makes QuickPay respect payment PIN settings, and defers payment inputs until authentication. It also adds focused tests and a security changelog entry.

  • Adds shared Bitrefill host/origin validation for navigation and WebView messages.
  • Prevents QuickPay from bypassing payment PIN confirmation.
  • Defers locked scans and deeplinks, although multiple pending inputs currently overwrite one another.
  • Adds unit coverage for origin filtering, navigation blocking, PIN-aware QuickPay, and single pending-input replay.

Confidence Score: 4/5

The PR should not merge until locked payment inputs can no longer silently overwrite earlier pending requests.

The new authentication gate retains only one pending scan or deeplink, so consecutive Android payment intents received before unlock cause earlier requests to be discarded.

Files Needing Attention: app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.ktIntroduces HTTPS Bitrefill host validation and an origin-filtering JavaScript message bridge.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewClient.ktInjects the restricted bridge and blocks untrusted main-frame navigation.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewInterface.ktMoves bridge handling onto the main thread and validates the current page origin before forwarding payment intents.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewScreen.ktSupplies the current WebView URL to the interface for origin validation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.ktAdds PIN-aware QuickPay suppression and locked-input deferral, but the single pending slot silently drops all but the latest input.
app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.ktCovers replay of one locked input and PIN-aware QuickPay but does not exercise multiple inputs received before unlock.

Sequence Diagram

sequenceDiagram
participant OS as Android intents
participant VM as AppViewModel
participant Lock as Authentication UI
participant Send as Send flow
OS->>VM: Payment input A
VM->>VM: "pendingLockedScan = A"
OS->>VM: Payment input B
VM->>VM: "pendingLockedScan = B"
Note over VM: Input A is overwritten
Lock->>VM: setIsAuthenticated(true)
VM->>VM: flushPendingLockedScan()
VM->>Send: Launch input B only
Loading

Reviews (1): Last reviewed commit: "fix: handle null shop origin scheme" | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated

@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 + manual verification on a regtest dev build (Pixel_9 emulator).

Verified working on device: QuickPay is skipped when PIN-for-payments is on (Skipping QuickPay because PIN is required for payments → Confirm sheet → PIN prompt on swipe); QuickPay still runs with PIN-for-payments off; payment links fired while locked are queued and opened exactly once after auth; a second link replaces the first (Replacing deferred scan) and the earlier one is never replayed; off-Bitrefill main-frame navigation is blocked (Blocked shop navigation to untrusted origin 'https://policies.google.com/privacy') while in-shop navigation is unaffected.

Not covered on device: Bitrefill checkout (dev flavor talks to the real embed.bitrefill.com, so a checkout emits a mainnet invoice a regtest wallet can't act on), the Paykit handoff race, and the scanner-ordering case.

5 inline findings below.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.kt Outdated
@pwltr

pwltr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note that QuickPay skipping PIN checks is assumed risk per design.

image

@ben-kaufmanben-kaufman changed the title fix: gate shop origin and pin payfix: secure shop payments and locked linksAug 14, 2026
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr Thanks for clarifying. Agreed—QuickPay is the explicit opt-in exception to per-payment PIN confirmation. I removed the new PIN-for-payments gate in 32b59a5, kept the app-authentication deferral for locked links, and updated the regression test, changelog, title, and PR description to state the accepted behavior accurately. The focused test, full unit suite, compile, and detekt checks are green.

@ovitrifovitrif mentioned this pull request Aug 15, 2026
10 tasks
@ovitrif

This comment was marked as outdated.

@ovitrif
ovitrif self-requested a review August 15, 2026 12:58
cursoragentand others added 12 commits August 15, 2026 20:27
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ovitrif
ovitrifforce-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192CompareAugust 15, 2026 18:28
@ovitrif
ovitrif requested a review from jvsena42August 15, 2026 18:34
@ovitrifovitrif added this to the 2.5.0 milestone Aug 15, 2026

@ovitrifovitrif 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.

The shop payment origin gate, fail-closed WebView messaging, and locked-link deferral look correct on 49ab192, including the latest-wins deferred scan and the accepted QuickPay PIN bypass.

@ovitrif
ovitrif merged commit 5ccc5c0 into masterAug 15, 2026
18 checks passed
@ovitrif
ovitrif deleted the cursor/fix-shop-quickpay-auth-86ae branch August 15, 2026 23:03
@ovitrif

Copy link
Copy Markdown
Collaborator

Emulator QA on to.bitkit.dev @ 5ccc5c038 (Pixel emulator-5554). PIN 1234, QuickPay on, spending 400000 sats. Self-pay of this wallet's invoices fails after QuickPay starts (DuplicatePayment); the check was that Confirm/QuickPay opened, not that the payment settled.

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
    Reached Zalando €20 cart. Did not submit. Dev shop talks to real embed.bitrefill.com (mainnet invoice on a regtest wallet).
  • 2a. Shop → navigate between legitimate Bitrefill pages: Gift Cards → eSIMs stayed on Bitrefill.
  • 2b. Shop → off-Bitrefill main-frame nav: https://evil.example/pay blocked. Toast: "This link can’t be opened from the shop."
  • 3. PIN on + PIN for payments on + QuickPay on → small LN invoice: QuickPay opened with no second PIN (500 sats).
  • 4. PIN on + PIN for payments off + QuickPay on → small LN invoice: QuickPay still opened (600 sats).
  • 5. Lock app → QuickPay-eligible lightning: link → authenticate: no send UI before PIN, then QuickPay once (700 sats).
  • 6. Lock app → two payment links → authenticate: 800 sat replaced, only 900 sat opened.
  • 7. Lock + incoming Paykit request + payment link: not run. Android has no proposePaymentRequest UI. Mutual contacts only enable Contact Pay (outgoing). Incoming requests come from Paykit Server / Locks (paykit-payment invoice), not Bitkit ↔ Bitkit.
  • 8. Lock + deferred lightning: link → open Scanner → authenticate → scan another invoice: scanner result opened first, deferred 400-sat QuickPay opened after dismiss.

Local artifacts: .ai/pr/1158/review/

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.

6 participants

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

fix: secure shop payments and locked links - #1158

Merged
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae
Aug 15, 2026
Merged

fix: secure shop payments and locked links#1158
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Restricts native shop payment messages to the exact https://embed.bitrefill.com origin through an origin-scoped WebMessage listener.
  2. Disables native shop payments when origin-aware WebView messaging is unavailable, rejects non-string messages, and keeps broader Bitrefill navigation separate from the privileged payment origin.
  3. Defers payment links and scans until authentication completes while preserving the intentional QuickPay opt-in behavior.
  4. Keeps only the latest deferred external scan while incoming Paykit requests remain repository-backed until authentication.
  5. Serializes scan execution and preserves each scan's contact context so concurrent requests cannot overlap or be attributed to the wrong contact.

Addresses VulnHunter 01b finding 02 and the unauthenticated deeplink handling from finding 03. The explicit QuickPay bypass of per-payment PIN confirmation remains accepted product behavior.

Companion iOS PR: synonymdev/bitkit-ios#668

Preview

N/A — security boundary and payment-flow state handling only.

QA Notes

Manual Tests

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
  • 2a.regression: Shop → navigate between legitimate Bitrefill pages: navigation remains available.
    • 2b. Shop → attempt off-Bitrefill main-frame navigation: navigation is blocked.
  • 3. Settings → PIN on + PIN for payments on + QuickPay on → authenticate, then scan a small LN invoice: QuickPay opens without another PIN prompt, as designed.
  • 4. Settings → PIN on + PIN for payments off + QuickPay on → scan a small LN invoice: QuickPay still opens.
  • 5. Lock app → open a QuickPay-eligible lightning: link → authenticate: no payment flow opens before authentication, then QuickPay opens once.
  • 6. Lock app → open two different payment links → authenticate: only the latest link opens and the earlier link is not replayed.
  • 7. Lock app with an incoming Paykit request → open a payment link → authenticate → dismiss the external send flow: the external payment opens first, then the Paykit request opens once with its contact context.
  • 8. Lock app with a payment link deferred → open Scanner → authenticate → scan another invoice: the scanner result opens first and the deferred link opens after it is dismissed.

Automated Checks

  • ShopOriginTest.kt: covers the exact payment origin, trusted port/user-info boundary, broader navigation allow-list, and bridge-page guard.
  • ShopWebViewInterfaceTest.kt: covers feature detection, fail-closed fallback, exact listener origin, sibling-origin rejection, non-string messages, and payment parsing.
  • ShopWebViewClientTest.kt: covers supported/unsupported bridge injection, sibling-page non-injection, and main-frame navigation boundaries.
  • AppViewModelSendFlowTest.kt: covers authentication/QuickPay deferral, latest-wins replacement, context isolation, cancellation serialization, scanner ordering, sheet dismissal, and Paykit handoff races.
  • Test coverage removed: deleted FIFO/capacity/stale-queue cases because deferred external scans now use one latest-wins slot and automatic Paykit requests remain repository-backed.
  • Local verification: compileDevDebugKotlin, testDevDebugUnitTest, detekt --rerun-tasks, and git diff --check.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
@ben-kaufman
ben-kaufman marked this pull request as ready for review August 13, 2026 19:00
@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR restricts the shop WebView to trusted Bitrefill HTTPS origins, makes QuickPay respect payment PIN settings, and defers payment inputs until authentication. It also adds focused tests and a security changelog entry.

  • Adds shared Bitrefill host/origin validation for navigation and WebView messages.
  • Prevents QuickPay from bypassing payment PIN confirmation.
  • Defers locked scans and deeplinks, although multiple pending inputs currently overwrite one another.
  • Adds unit coverage for origin filtering, navigation blocking, PIN-aware QuickPay, and single pending-input replay.

Confidence Score: 4/5

The PR should not merge until locked payment inputs can no longer silently overwrite earlier pending requests.

The new authentication gate retains only one pending scan or deeplink, so consecutive Android payment intents received before unlock cause earlier requests to be discarded.

Files Needing Attention: app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.ktIntroduces HTTPS Bitrefill host validation and an origin-filtering JavaScript message bridge.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewClient.ktInjects the restricted bridge and blocks untrusted main-frame navigation.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewInterface.ktMoves bridge handling onto the main thread and validates the current page origin before forwarding payment intents.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewScreen.ktSupplies the current WebView URL to the interface for origin validation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.ktAdds PIN-aware QuickPay suppression and locked-input deferral, but the single pending slot silently drops all but the latest input.
app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.ktCovers replay of one locked input and PIN-aware QuickPay but does not exercise multiple inputs received before unlock.

Sequence Diagram

sequenceDiagram
participant OS as Android intents
participant VM as AppViewModel
participant Lock as Authentication UI
participant Send as Send flow
OS->>VM: Payment input A
VM->>VM: "pendingLockedScan = A"
OS->>VM: Payment input B
VM->>VM: "pendingLockedScan = B"
Note over VM: Input A is overwritten
Lock->>VM: setIsAuthenticated(true)
VM->>VM: flushPendingLockedScan()
VM->>Send: Launch input B only
Loading

Reviews (1): Last reviewed commit: "fix: handle null shop origin scheme" | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated

@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 + manual verification on a regtest dev build (Pixel_9 emulator).

Verified working on device: QuickPay is skipped when PIN-for-payments is on (Skipping QuickPay because PIN is required for payments → Confirm sheet → PIN prompt on swipe); QuickPay still runs with PIN-for-payments off; payment links fired while locked are queued and opened exactly once after auth; a second link replaces the first (Replacing deferred scan) and the earlier one is never replayed; off-Bitrefill main-frame navigation is blocked (Blocked shop navigation to untrusted origin 'https://policies.google.com/privacy') while in-shop navigation is unaffected.

Not covered on device: Bitrefill checkout (dev flavor talks to the real embed.bitrefill.com, so a checkout emits a mainnet invoice a regtest wallet can't act on), the Paykit handoff race, and the scanner-ordering case.

5 inline findings below.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.kt Outdated
@pwltr

pwltr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note that QuickPay skipping PIN checks is assumed risk per design.

image

@ben-kaufmanben-kaufman changed the title fix: gate shop origin and pin payfix: secure shop payments and locked linksAug 14, 2026
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr Thanks for clarifying. Agreed—QuickPay is the explicit opt-in exception to per-payment PIN confirmation. I removed the new PIN-for-payments gate in 32b59a5, kept the app-authentication deferral for locked links, and updated the regression test, changelog, title, and PR description to state the accepted behavior accurately. The focused test, full unit suite, compile, and detekt checks are green.

@ovitrifovitrif mentioned this pull request Aug 15, 2026
10 tasks
@ovitrif

This comment was marked as outdated.

@ovitrif
ovitrif self-requested a review August 15, 2026 12:58
cursoragentand others added 12 commits August 15, 2026 20:27
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ovitrif
ovitrifforce-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192CompareAugust 15, 2026 18:28
@ovitrif
ovitrif requested a review from jvsena42August 15, 2026 18:34
@ovitrifovitrif added this to the 2.5.0 milestone Aug 15, 2026

@ovitrifovitrif 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.

The shop payment origin gate, fail-closed WebView messaging, and locked-link deferral look correct on 49ab192, including the latest-wins deferred scan and the accepted QuickPay PIN bypass.

@ovitrif
ovitrif merged commit 5ccc5c0 into masterAug 15, 2026
18 checks passed
@ovitrif
ovitrif deleted the cursor/fix-shop-quickpay-auth-86ae branch August 15, 2026 23:03
@ovitrif

Copy link
Copy Markdown
Collaborator

Emulator QA on to.bitkit.dev @ 5ccc5c038 (Pixel emulator-5554). PIN 1234, QuickPay on, spending 400000 sats. Self-pay of this wallet's invoices fails after QuickPay starts (DuplicatePayment); the check was that Confirm/QuickPay opened, not that the payment settled.

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
    Reached Zalando €20 cart. Did not submit. Dev shop talks to real embed.bitrefill.com (mainnet invoice on a regtest wallet).
  • 2a. Shop → navigate between legitimate Bitrefill pages: Gift Cards → eSIMs stayed on Bitrefill.
  • 2b. Shop → off-Bitrefill main-frame nav: https://evil.example/pay blocked. Toast: "This link can’t be opened from the shop."
  • 3. PIN on + PIN for payments on + QuickPay on → small LN invoice: QuickPay opened with no second PIN (500 sats).
  • 4. PIN on + PIN for payments off + QuickPay on → small LN invoice: QuickPay still opened (600 sats).
  • 5. Lock app → QuickPay-eligible lightning: link → authenticate: no send UI before PIN, then QuickPay once (700 sats).
  • 6. Lock app → two payment links → authenticate: 800 sat replaced, only 900 sat opened.
  • 7. Lock + incoming Paykit request + payment link: not run. Android has no proposePaymentRequest UI. Mutual contacts only enable Contact Pay (outgoing). Incoming requests come from Paykit Server / Locks (paykit-payment invoice), not Bitkit ↔ Bitkit.
  • 8. Lock + deferred lightning: link → open Scanner → authenticate → scan another invoice: scanner result opened first, deferred 400-sat QuickPay opened after dismiss.

Local artifacts: .ai/pr/1158/review/

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.

6 participants

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

fix: secure shop payments and locked links - #1158

Merged
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae
Aug 15, 2026
Merged

fix: secure shop payments and locked links#1158
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Restricts native shop payment messages to the exact https://embed.bitrefill.com origin through an origin-scoped WebMessage listener.
  2. Disables native shop payments when origin-aware WebView messaging is unavailable, rejects non-string messages, and keeps broader Bitrefill navigation separate from the privileged payment origin.
  3. Defers payment links and scans until authentication completes while preserving the intentional QuickPay opt-in behavior.
  4. Keeps only the latest deferred external scan while incoming Paykit requests remain repository-backed until authentication.
  5. Serializes scan execution and preserves each scan's contact context so concurrent requests cannot overlap or be attributed to the wrong contact.

Addresses VulnHunter 01b finding 02 and the unauthenticated deeplink handling from finding 03. The explicit QuickPay bypass of per-payment PIN confirmation remains accepted product behavior.

Companion iOS PR: synonymdev/bitkit-ios#668

Preview

N/A — security boundary and payment-flow state handling only.

QA Notes

Manual Tests

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
  • 2a.regression: Shop → navigate between legitimate Bitrefill pages: navigation remains available.
    • 2b. Shop → attempt off-Bitrefill main-frame navigation: navigation is blocked.
  • 3. Settings → PIN on + PIN for payments on + QuickPay on → authenticate, then scan a small LN invoice: QuickPay opens without another PIN prompt, as designed.
  • 4. Settings → PIN on + PIN for payments off + QuickPay on → scan a small LN invoice: QuickPay still opens.
  • 5. Lock app → open a QuickPay-eligible lightning: link → authenticate: no payment flow opens before authentication, then QuickPay opens once.
  • 6. Lock app → open two different payment links → authenticate: only the latest link opens and the earlier link is not replayed.
  • 7. Lock app with an incoming Paykit request → open a payment link → authenticate → dismiss the external send flow: the external payment opens first, then the Paykit request opens once with its contact context.
  • 8. Lock app with a payment link deferred → open Scanner → authenticate → scan another invoice: the scanner result opens first and the deferred link opens after it is dismissed.

Automated Checks

  • ShopOriginTest.kt: covers the exact payment origin, trusted port/user-info boundary, broader navigation allow-list, and bridge-page guard.
  • ShopWebViewInterfaceTest.kt: covers feature detection, fail-closed fallback, exact listener origin, sibling-origin rejection, non-string messages, and payment parsing.
  • ShopWebViewClientTest.kt: covers supported/unsupported bridge injection, sibling-page non-injection, and main-frame navigation boundaries.
  • AppViewModelSendFlowTest.kt: covers authentication/QuickPay deferral, latest-wins replacement, context isolation, cancellation serialization, scanner ordering, sheet dismissal, and Paykit handoff races.
  • Test coverage removed: deleted FIFO/capacity/stale-queue cases because deferred external scans now use one latest-wins slot and automatic Paykit requests remain repository-backed.
  • Local verification: compileDevDebugKotlin, testDevDebugUnitTest, detekt --rerun-tasks, and git diff --check.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
@ben-kaufman
ben-kaufman marked this pull request as ready for review August 13, 2026 19:00
@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR restricts the shop WebView to trusted Bitrefill HTTPS origins, makes QuickPay respect payment PIN settings, and defers payment inputs until authentication. It also adds focused tests and a security changelog entry.

  • Adds shared Bitrefill host/origin validation for navigation and WebView messages.
  • Prevents QuickPay from bypassing payment PIN confirmation.
  • Defers locked scans and deeplinks, although multiple pending inputs currently overwrite one another.
  • Adds unit coverage for origin filtering, navigation blocking, PIN-aware QuickPay, and single pending-input replay.

Confidence Score: 4/5

The PR should not merge until locked payment inputs can no longer silently overwrite earlier pending requests.

The new authentication gate retains only one pending scan or deeplink, so consecutive Android payment intents received before unlock cause earlier requests to be discarded.

Files Needing Attention: app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.ktIntroduces HTTPS Bitrefill host validation and an origin-filtering JavaScript message bridge.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewClient.ktInjects the restricted bridge and blocks untrusted main-frame navigation.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewInterface.ktMoves bridge handling onto the main thread and validates the current page origin before forwarding payment intents.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewScreen.ktSupplies the current WebView URL to the interface for origin validation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.ktAdds PIN-aware QuickPay suppression and locked-input deferral, but the single pending slot silently drops all but the latest input.
app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.ktCovers replay of one locked input and PIN-aware QuickPay but does not exercise multiple inputs received before unlock.

Sequence Diagram

sequenceDiagram
participant OS as Android intents
participant VM as AppViewModel
participant Lock as Authentication UI
participant Send as Send flow
OS->>VM: Payment input A
VM->>VM: "pendingLockedScan = A"
OS->>VM: Payment input B
VM->>VM: "pendingLockedScan = B"
Note over VM: Input A is overwritten
Lock->>VM: setIsAuthenticated(true)
VM->>VM: flushPendingLockedScan()
VM->>Send: Launch input B only
Loading

Reviews (1): Last reviewed commit: "fix: handle null shop origin scheme" | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated

@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 + manual verification on a regtest dev build (Pixel_9 emulator).

Verified working on device: QuickPay is skipped when PIN-for-payments is on (Skipping QuickPay because PIN is required for payments → Confirm sheet → PIN prompt on swipe); QuickPay still runs with PIN-for-payments off; payment links fired while locked are queued and opened exactly once after auth; a second link replaces the first (Replacing deferred scan) and the earlier one is never replayed; off-Bitrefill main-frame navigation is blocked (Blocked shop navigation to untrusted origin 'https://policies.google.com/privacy') while in-shop navigation is unaffected.

Not covered on device: Bitrefill checkout (dev flavor talks to the real embed.bitrefill.com, so a checkout emits a mainnet invoice a regtest wallet can't act on), the Paykit handoff race, and the scanner-ordering case.

5 inline findings below.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.kt Outdated
@pwltr

pwltr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note that QuickPay skipping PIN checks is assumed risk per design.

image

@ben-kaufmanben-kaufman changed the title fix: gate shop origin and pin payfix: secure shop payments and locked linksAug 14, 2026
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr Thanks for clarifying. Agreed—QuickPay is the explicit opt-in exception to per-payment PIN confirmation. I removed the new PIN-for-payments gate in 32b59a5, kept the app-authentication deferral for locked links, and updated the regression test, changelog, title, and PR description to state the accepted behavior accurately. The focused test, full unit suite, compile, and detekt checks are green.

@ovitrifovitrif mentioned this pull request Aug 15, 2026
10 tasks
@ovitrif

This comment was marked as outdated.

@ovitrif
ovitrif self-requested a review August 15, 2026 12:58
cursoragentand others added 12 commits August 15, 2026 20:27
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ovitrif
ovitrifforce-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192CompareAugust 15, 2026 18:28
@ovitrif
ovitrif requested a review from jvsena42August 15, 2026 18:34
@ovitrifovitrif added this to the 2.5.0 milestone Aug 15, 2026

@ovitrifovitrif 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.

The shop payment origin gate, fail-closed WebView messaging, and locked-link deferral look correct on 49ab192, including the latest-wins deferred scan and the accepted QuickPay PIN bypass.

@ovitrif
ovitrif merged commit 5ccc5c0 into masterAug 15, 2026
18 checks passed
@ovitrif
ovitrif deleted the cursor/fix-shop-quickpay-auth-86ae branch August 15, 2026 23:03
@ovitrif

Copy link
Copy Markdown
Collaborator

Emulator QA on to.bitkit.dev @ 5ccc5c038 (Pixel emulator-5554). PIN 1234, QuickPay on, spending 400000 sats. Self-pay of this wallet's invoices fails after QuickPay starts (DuplicatePayment); the check was that Confirm/QuickPay opened, not that the payment settled.

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
    Reached Zalando €20 cart. Did not submit. Dev shop talks to real embed.bitrefill.com (mainnet invoice on a regtest wallet).
  • 2a. Shop → navigate between legitimate Bitrefill pages: Gift Cards → eSIMs stayed on Bitrefill.
  • 2b. Shop → off-Bitrefill main-frame nav: https://evil.example/pay blocked. Toast: "This link can’t be opened from the shop."
  • 3. PIN on + PIN for payments on + QuickPay on → small LN invoice: QuickPay opened with no second PIN (500 sats).
  • 4. PIN on + PIN for payments off + QuickPay on → small LN invoice: QuickPay still opened (600 sats).
  • 5. Lock app → QuickPay-eligible lightning: link → authenticate: no send UI before PIN, then QuickPay once (700 sats).
  • 6. Lock app → two payment links → authenticate: 800 sat replaced, only 900 sat opened.
  • 7. Lock + incoming Paykit request + payment link: not run. Android has no proposePaymentRequest UI. Mutual contacts only enable Contact Pay (outgoing). Incoming requests come from Paykit Server / Locks (paykit-payment invoice), not Bitkit ↔ Bitkit.
  • 8. Lock + deferred lightning: link → open Scanner → authenticate → scan another invoice: scanner result opened first, deferred 400-sat QuickPay opened after dismiss.

Local artifacts: .ai/pr/1158/review/

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.

6 participants

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

fix: secure shop payments and locked links - #1158

Merged
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae
Aug 15, 2026
Merged

fix: secure shop payments and locked links#1158
ovitrif merged 12 commits into
masterfrom
cursor/fix-shop-quickpay-auth-86ae

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

This PR:

  1. Restricts native shop payment messages to the exact https://embed.bitrefill.com origin through an origin-scoped WebMessage listener.
  2. Disables native shop payments when origin-aware WebView messaging is unavailable, rejects non-string messages, and keeps broader Bitrefill navigation separate from the privileged payment origin.
  3. Defers payment links and scans until authentication completes while preserving the intentional QuickPay opt-in behavior.
  4. Keeps only the latest deferred external scan while incoming Paykit requests remain repository-backed until authentication.
  5. Serializes scan execution and preserves each scan's contact context so concurrent requests cannot overlap or be attributed to the wrong contact.

Addresses VulnHunter 01b finding 02 and the unauthenticated deeplink handling from finding 03. The explicit QuickPay bypass of per-payment PIN confirmation remains accepted product behavior.

Companion iOS PR: synonymdev/bitkit-ios#668

Preview

N/A — security boundary and payment-flow state handling only.

QA Notes

Manual Tests

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
  • 2a.regression: Shop → navigate between legitimate Bitrefill pages: navigation remains available.
    • 2b. Shop → attempt off-Bitrefill main-frame navigation: navigation is blocked.
  • 3. Settings → PIN on + PIN for payments on + QuickPay on → authenticate, then scan a small LN invoice: QuickPay opens without another PIN prompt, as designed.
  • 4. Settings → PIN on + PIN for payments off + QuickPay on → scan a small LN invoice: QuickPay still opens.
  • 5. Lock app → open a QuickPay-eligible lightning: link → authenticate: no payment flow opens before authentication, then QuickPay opens once.
  • 6. Lock app → open two different payment links → authenticate: only the latest link opens and the earlier link is not replayed.
  • 7. Lock app with an incoming Paykit request → open a payment link → authenticate → dismiss the external send flow: the external payment opens first, then the Paykit request opens once with its contact context.
  • 8. Lock app with a payment link deferred → open Scanner → authenticate → scan another invoice: the scanner result opens first and the deferred link opens after it is dismissed.

Automated Checks

  • ShopOriginTest.kt: covers the exact payment origin, trusted port/user-info boundary, broader navigation allow-list, and bridge-page guard.
  • ShopWebViewInterfaceTest.kt: covers feature detection, fail-closed fallback, exact listener origin, sibling-origin rejection, non-string messages, and payment parsing.
  • ShopWebViewClientTest.kt: covers supported/unsupported bridge injection, sibling-page non-injection, and main-frame navigation boundaries.
  • AppViewModelSendFlowTest.kt: covers authentication/QuickPay deferral, latest-wins replacement, context isolation, cancellation serialization, scanner ordering, sheet dismissal, and Paykit handoff races.
  • Test coverage removed: deleted FIFO/capacity/stale-queue cases because deferred external scans now use one latest-wins slot and automatic Paykit requests remain repository-backed.
  • Local verification: compileDevDebugKotlin, testDevDebugUnitTest, detekt --rerun-tasks, and git diff --check.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Fixed
@ben-kaufman
ben-kaufman marked this pull request as ready for review August 13, 2026 19:00
@greptile-apps

Copy link
Copy Markdown

Greptile Summary

This PR restricts the shop WebView to trusted Bitrefill HTTPS origins, makes QuickPay respect payment PIN settings, and defers payment inputs until authentication. It also adds focused tests and a security changelog entry.

  • Adds shared Bitrefill host/origin validation for navigation and WebView messages.
  • Prevents QuickPay from bypassing payment PIN confirmation.
  • Defers locked scans and deeplinks, although multiple pending inputs currently overwrite one another.
  • Adds unit coverage for origin filtering, navigation blocking, PIN-aware QuickPay, and single pending-input replay.

Confidence Score: 4/5

The PR should not merge until locked payment inputs can no longer silently overwrite earlier pending requests.

The new authentication gate retains only one pending scan or deeplink, so consecutive Android payment intents received before unlock cause earlier requests to be discarded.

Files Needing Attention: app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.ktIntroduces HTTPS Bitrefill host validation and an origin-filtering JavaScript message bridge.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewClient.ktInjects the restricted bridge and blocks untrusted main-frame navigation.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewInterface.ktMoves bridge handling onto the main thread and validates the current page origin before forwarding payment intents.
app/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopWebViewScreen.ktSupplies the current WebView URL to the interface for origin validation.
app/src/main/java/to/bitkit/viewmodels/AppViewModel.ktAdds PIN-aware QuickPay suppression and locked-input deferral, but the single pending slot silently drops all but the latest input.
app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.ktCovers replay of one locked input and PIN-aware QuickPay but does not exercise multiple inputs received before unlock.

Sequence Diagram

sequenceDiagram
participant OS as Android intents
participant VM as AppViewModel
participant Lock as Authentication UI
participant Send as Send flow
OS->>VM: Payment input A
VM->>VM: "pendingLockedScan = A"
OS->>VM: Payment input B
VM->>VM: "pendingLockedScan = B"
Note over VM: Input A is overwritten
Lock->>VM: setIsAuthenticated(true)
VM->>VM: flushPendingLockedScan()
VM->>Send: Launch input B only
Loading

Reviews (1): Last reviewed commit: "fix: handle null shop origin scheme" | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated

@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 + manual verification on a regtest dev build (Pixel_9 emulator).

Verified working on device: QuickPay is skipped when PIN-for-payments is on (Skipping QuickPay because PIN is required for payments → Confirm sheet → PIN prompt on swipe); QuickPay still runs with PIN-for-payments off; payment links fired while locked are queued and opened exactly once after auth; a second link replaces the first (Replacing deferred scan) and the earlier one is never replayed; off-Bitrefill main-frame navigation is blocked (Blocked shop navigation to untrusted origin 'https://policies.google.com/privacy') while in-shop navigation is unaffected.

Not covered on device: Bitrefill checkout (dev flavor talks to the real embed.bitrefill.com, so a checkout emits a mainnet invoice a regtest wallet can't act on), the Paykit handoff race, and the scanner-ordering case.

5 inline findings below.

Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Comment threadapp/src/main/java/to/bitkit/ui/screens/shop/shopWebView/ShopOrigin.kt Outdated
@pwltr

pwltr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note that QuickPay skipping PIN checks is assumed risk per design.

image

@ben-kaufmanben-kaufman changed the title fix: gate shop origin and pin payfix: secure shop payments and locked linksAug 14, 2026
@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr Thanks for clarifying. Agreed—QuickPay is the explicit opt-in exception to per-payment PIN confirmation. I removed the new PIN-for-payments gate in 32b59a5, kept the app-authentication deferral for locked links, and updated the regression test, changelog, title, and PR description to state the accepted behavior accurately. The focused test, full unit suite, compile, and detekt checks are green.

@ovitrifovitrif mentioned this pull request Aug 15, 2026
10 tasks
@ovitrif

This comment was marked as outdated.

@ovitrif
ovitrif self-requested a review August 15, 2026 12:58
cursoragentand others added 12 commits August 15, 2026 20:27
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ovitrif
ovitrifforce-pushed the cursor/fix-shop-quickpay-auth-86ae branch from 32b59a5 to 49ab192CompareAugust 15, 2026 18:28
@ovitrif
ovitrif requested a review from jvsena42August 15, 2026 18:34
@ovitrifovitrif added this to the 2.5.0 milestone Aug 15, 2026

@ovitrifovitrif 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.

The shop payment origin gate, fail-closed WebView messaging, and locked-link deferral look correct on 49ab192, including the latest-wins deferred scan and the accepted QuickPay PIN bypass.

@ovitrif
ovitrif merged commit 5ccc5c0 into masterAug 15, 2026
18 checks passed
@ovitrif
ovitrif deleted the cursor/fix-shop-quickpay-auth-86ae branch August 15, 2026 23:03
@ovitrif

Copy link
Copy Markdown
Collaborator

Emulator QA on to.bitkit.dev @ 5ccc5c038 (Pixel emulator-5554). PIN 1234, QuickPay on, spending 400000 sats. Self-pay of this wallet's invoices fails after QuickPay starts (DuplicatePayment); the check was that Confirm/QuickPay opened, not that the payment settled.

  • 1. Shop → Gift Cards → Bitrefill checkout → submit a payment: Send Confirm or QuickPay opens with the expected invoice.
    Reached Zalando €20 cart. Did not submit. Dev shop talks to real embed.bitrefill.com (mainnet invoice on a regtest wallet).
  • 2a. Shop → navigate between legitimate Bitrefill pages: Gift Cards → eSIMs stayed on Bitrefill.
  • 2b. Shop → off-Bitrefill main-frame nav: https://evil.example/pay blocked. Toast: "This link can’t be opened from the shop."
  • 3. PIN on + PIN for payments on + QuickPay on → small LN invoice: QuickPay opened with no second PIN (500 sats).
  • 4. PIN on + PIN for payments off + QuickPay on → small LN invoice: QuickPay still opened (600 sats).
  • 5. Lock app → QuickPay-eligible lightning: link → authenticate: no send UI before PIN, then QuickPay once (700 sats).
  • 6. Lock app → two payment links → authenticate: 800 sat replaced, only 900 sat opened.
  • 7. Lock + incoming Paykit request + payment link: not run. Android has no proposePaymentRequest UI. Mutual contacts only enable Contact Pay (outgoing). Incoming requests come from Paykit Server / Locks (paykit-payment invoice), not Bitkit ↔ Bitkit.
  • 8. Lock + deferred lightning: link → open Scanner → authenticate → scan another invoice: scanner result opened first, deferred 400-sat QuickPay opened after dismiss.

Local artifacts: .ai/pr/1158/review/

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.

6 participants

@ben-kaufman@pwltr@ovitrif@jvsena42@github-advanced-security@cursoragent