Skip to content

fix(buy): stop the first NIM payment failing on a blocked popup - #22

Merged
Samuel1-ona merged 1 commit into
mainfrom
samuel1-ona/first-nim-payment-blocked-by-popup
Sep 9, 2026
Merged

fix(buy): stop the first NIM payment failing on a blocked popup#22
Samuel1-ona merged 1 commit into
mainfrom
samuel1-ona/first-nim-payment-blocked-by-popup

Conversation

@Samuel1-ona

Copy link
Copy Markdown
Contributor

Reported from a browser: the first tap on PAY WITH NIM failed with "The NIM payment was not completed", and trying again a moment later worked.

sendNimViaHub awaits loadHub() before calling checkout(). On the first payment of a session that await is a dynamic import('@nimiq/hub-api') over the NETWORK, and crossing a task boundary ends the user activation the tap granted. checkout() opens a popup, browsers only allow that from inside a gesture, so the popup was blocked and the Hub rejected with nothing usable in it. The retry worked because by then the module was cached and the await resolved within the gesture — which is exactly the reported shape, and why it looked intermittent rather than broken.

Adds preloadNimiqHub(), called from NimPayPanel as soon as it detects a browser host, so the module is fetched long before anyone taps. A script fetch raises no dialog, so this does not fall foul of the mini-app rule against confirmations on load. The warm swallows its own errors: it is an optimisation, and the real payment reports its own failures.

Also fixes the message that made this hard to report. A Hub rejection carrying no message is far more often a blocked popup than a deliberate cancel, so the fallback now says so — "The Nimiq Wallet window did not open. Check that pop-ups are allowed for this site, then try again." — instead of "not completed", which named no cause and suggested no action. A message the Hub does supply still passes through untouched, and there is now a test for each.

Verification: lint clean, tsc --noEmit clean, 1066 tests pass (5 new). Mutation-tested: making the preload a no-op turns 2 red, including a control asserting that WITHOUT a warm the payment itself is what builds the Hub — the state the bug happened in.

Claude-Session: https://claude.ai/code/session_01Pa7dcWrqWwkwZE2mZWSG1P

The hole, and the fix

What this does NOT do / residual risk

Judgement calls

Issues

Closes #
Refs #

Stacking / conflicts

Verification evidence

Remaining ops steps

  • none

Checklist

  • Title is the commit message I want on main
  • Ships the test that fails on pre-fix code; fixtures are real captured data; fakes honour their arguments
  • Covers the seam, not only pure functions; any stated guarantee tested on its failure path
  • Error responses audited for what they leak; reads on write paths bounded at the query
  • Judgement calls / bundled product changes flagged above (or "none")
  • lint / typecheck / test pass locally on the current head
  • Re-read acceptance criteria of every Closes issue — all met
  • Development sidebar links match Closes/Refs above
  • README / runbook / .env.example / examples / error strings updated for the world this creates
  • Lockfile touched → rebased on current main, lockfile regenerated (never hand-resolved)
  • Wallet/provider tree touched → loaded in a normal browser, not only Nimiq Pay
  • Money/security path → money-path-checklist.md run; payout logic compared against the other side
  • No secrets in the diff
  • Questions for the maintainer marked clearly at the end (or "none")

Reported from a browser: the first tap on PAY WITH NIM failed with "The NIM
payment was not completed", and trying again a moment later worked.

`sendNimViaHub` awaits `loadHub()` before calling `checkout()`. On the first
payment of a session that await is a dynamic `import('@nimiq/hub-api')` over
the NETWORK, and crossing a task boundary ends the user activation the tap
granted. `checkout()` opens a popup, browsers only allow that from inside a
gesture, so the popup was blocked and the Hub rejected with nothing usable in
it. The retry worked because by then the module was cached and the await
resolved within the gesture — which is exactly the reported shape, and why it
looked intermittent rather than broken.

Adds `preloadNimiqHub()`, called from `NimPayPanel` as soon as it detects a
browser host, so the module is fetched long before anyone taps. A script fetch
raises no dialog, so this does not fall foul of the mini-app rule against
confirmations on load. The warm swallows its own errors: it is an
optimisation, and the real payment reports its own failures.

Also fixes the message that made this hard to report. A Hub rejection carrying
no message is far more often a blocked popup than a deliberate cancel, so the
fallback now says so — "The Nimiq Wallet window did not open. Check that
pop-ups are allowed for this site, then try again." — instead of "not
completed", which named no cause and suggested no action. A message the Hub
does supply still passes through untouched, and there is now a test for each.

Verification: lint clean, `tsc --noEmit` clean, 1066 tests pass (5 new).
Mutation-tested: making the preload a no-op turns 2 red, including a control
asserting that WITHOUT a warm the payment itself is what builds the Hub — the
state the bug happened in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pa7dcWrqWwkwZE2mZWSG1P
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
terreno-web Ready Ready Preview Sep 9, 2026 2:44pm UTC

Request Review

@Samuel1-ona
Samuel1-ona merged commit ad5146a into main Sep 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant