Skip to content

feat: username-customized deep links (/username -> auto-generated donation button) - #10

Merged
pretyflaco merged 2 commits into
mainfrom
feat/username-deep-link
Jul 20, 2026
Merged

pretyflaco merged 2 commits into
mainfrom
feat/username-deep-link

Conversation

@pretyflaco

Copy link
Copy Markdown
Collaborator

What

Adds support for username-customized deep links so external surfaces can send a user straight to their own pre-generated donation button:

https://donation-button.blink.sv/pretyflaco

The primary motivator is linking from the Blink mobile app's "Ways to get paid" section directly to a user's donation button.

How (GitHub Pages has no server-side routing)

  • 404.html (new): GitHub Pages serves this for any unmatched path. It validates the path as a plausible single-segment username, stashes it in sessionStorage, and location.replace('/') to the generator (so the 404 URL doesn't linger in history). Dependency-free, inline, runs before render.
  • js/generator.js: two new pure, unit-testable helpers:
    • parseUsernameFromPath(pathname) — path → candidate username, or null for root / nested / file-like / reserved / invalid segments.
    • resolveDeepLinkUsername(win) — prefers the 404 sessionStorage handoff (consumed once so a reload doesn't re-trigger), falls back to location.pathname.
    • On DOMContentLoaded, if a username resolves, it prefills the input and calls generateCode() — so existence is still verified against Blink (incl. the Spark LNURL fallback) and unknown usernames show the normal "does not exist yet" message.
  • DOM wiring is now guarded behind typeof document and the helpers are exported UMD-style, so the file can be required without a DOM for tests (mirrors the blink-lnurl.js pattern).

Behavior on landing

/pretyflaco → prefill + auto-generate (verified existence check runs). Root / is unchanged. Reserved paths (/js, /img, /css, /tests, /404, /CNAME, ...), files (/favicon.ico, *.html/js/css), nested paths, and invalid segments do not trigger a username and just load the generator home.

Testing

  • tests/generator-deeplink.spec.js (new, 18 cases): parsing edge cases + the sessionStorage/path precedence, one-time consumption, and failure modes (storage throwing / no window).
  • npm test green — 104 passing (was 86).
  • Manual browser smoke: simulated the 404 handoff (set sessionStorage.blinkRedirectUsername = 'pretyflaco', load /). Result: input prefilled to pretyflaco, existence check passed (✓ Blink username found!), result container shown, embed code generated, and the sessionStorage key consumed.

Not changed

Public API (BlinkPayButton.init), blink-pay-button-container id, the single-file embed path, and analytics are all untouched.

Part 2 of 2. Part 1 (copy consistency) is #9.

…ton)

Support linking straight to a pre-generated donation button via a
username-customized URL, e.g. https://donation-button.blink.sv/pretyflaco.
This lets external surfaces (notably the Blink mobile app's "Ways to get
paid" section) send a user directly to their own button.

GitHub Pages has no server-side routing, so:

- 404.html captures the requested path, validates it as a plausible
  single-segment username, stashes it in sessionStorage, and redirects to
  "/" (history-replacing, so the 404 URL doesn't linger).
- js/generator.js gains two pure, unit-testable helpers:
    * parseUsernameFromPath(pathname) -- path -> candidate username, or null
      for root/nested/file-like/reserved/invalid segments.
    * resolveDeepLinkUsername(win) -- prefers the 404 sessionStorage handoff
      (consumed once), falls back to location.pathname.
  On DOMContentLoaded, if a username is resolved, it prefills the input and
  calls generateCode(), so existence is still verified against Blink (with
  the Spark LNURL fallback) and unknown usernames show the normal message.

The DOM wiring is now guarded behind "typeof document", and the helpers are
exported UMD-style so they can be required without a DOM. Adds
tests/generator-deeplink.spec.js (18 cases) covering parsing edge cases and
the sessionStorage/path precedence + failure modes.

Public API, container id, embed path, and analytics are unchanged.
npm test green (104 passing). Verified end-to-end in a browser: a stored
username prefills + auto-generates and the sessionStorage key is consumed.

@blink-dev-bot blink-dev-bot 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.

Review: feat: username-customized deep links

Verdict: APPROVE

Clean, well-structured PR. The 404.html → sessionStorage → generator handoff is the right pattern for GitHub Pages' lack of server-side routing. Good test coverage (18 cases), proper edge case handling, and the username is still verified against the Blink API before rendering.

Observations

MEDIUM — Duplicated parseUsernameFromPath logic
The username parser is duplicated between 404.html (ES5 inline) and js/generator.js (modern JS). The comment says "keep the two in sync" but code duplication is a maintenance risk. Not blocking since the 404.html version is intentionally dependency-free and inline for load speed, but worth noting — if the validation rules ever change, both need updating.

LOW — Incomplete fallback comment in 404.html
Line 64: "fall back to a query param the generator can also read is not implemented; just proceed to home." — reads like a TODO that was left as-is. Consider rewording to clarify this is a deliberate choice, not a pending feature.

LOW — Reserved list maintenance
Both files maintain identical reserved word lists. If either grows (e.g. adding fonts, api, docs), the other must match. A comment cross-referencing the two locations would help.

What's good

  • Pure, unit-testable helpers exported UMD-style — clean separation
  • sessionStorage consumed once to prevent re-trigger on reload
  • Graceful degradation when sessionStorage is unavailable
  • <meta name="robots" content="noindex"> on the 404 page prevents search engine indexing of redirect pages
  • Existence verification still runs through the Blink API — the deep link is a prefill shortcut, not a trust boundary

No blocking issues. Ship it.

🤖 Review by claude-opus-4-6 via Blink-Claw-Bot

…est)

Addresses the three non-blocking observations from the PR #10 review:

- LOW: reworded the 404.html sessionStorage-catch comment. It read like a
  leftover TODO ("fall back to a query param ... is not implemented"); it now
  states the deliberate choice — no query-param fallback, just land on the
  generator home.
- LOW: added cross-reference comments to both reserved lists / parser copies
  (404.html inline ES5 and js/generator.js), noting the duplication is
  intentional (404.html must be dependency-free and run before render on
  GitHub Pages) and pointing at the new parity test.
- MEDIUM (duplicated parseUsernameFromPath): kept the intentional duplication
  but added tests/generator-deeplink-parity.spec.js, which extracts the inline
  parser from 404.html and asserts it agrees with js/generator.js's exported
  parser across a shared fixture table. Drift now fails CI (mirrors the
  inline-LNURL parity approach in AGENTS.md).

No runtime/behavior change. npm test green (133 passing, was 104).
@pretyflaco

Copy link
Copy Markdown
Collaborator Author

Thanks for the review! Addressed all three observations in 7c1676d:

  • LOW — confusing fallback comment (404.html): reworded. It read like a leftover TODO ("fall back to a query param ... is not implemented") — now it states the deliberate choice: no query-param fallback, the visitor just lands on the generator home and can type their username.
  • LOW — reserved-list maintenance: added cross-reference comments at both reserved lists (inline ES5 in 404.html and js/generator.js), noting the duplication is intentional and pointing at the new parity test so anyone editing one is reminded about the other.
  • MEDIUM — duplicated parseUsernameFromPath: kept the intentional duplication (404.html must be dependency-free and run before render on GitHub Pages — no build step), but added tests/generator-deeplink-parity.spec.js. It extracts the inline parser from 404.html and asserts it agrees with the js/generator.js export across a shared fixture table, so any future drift fails CI. This mirrors the inline-LNURL parity enforcement described in AGENTS.md.

No runtime/behavior change. npm test green — 133 passing (was 104).

@pretyflaco
pretyflaco merged commit d6a5f85 into main Jul 20, 2026
2 checks passed
@pretyflaco
pretyflaco deleted the feat/username-deep-link branch July 20, 2026 21:39
pretyflaco added a commit that referenced this pull request Jul 20, 2026
…#11)

* fix: deep-link username swallowed when DOMContentLoaded already fired

Following the deep-link feature (#10), hitting a username URL like
https://donation-button.blink.sv/pretyflaco intermittently landed on the plain
generator with the username silently dropped, instead of auto-generating.

Root cause: js/generator.js is loaded via a plain <script> at the end of
<body> (no `defer`) and is also reached through the 404.html ->
location.replace('/') deep-link redirect. In that redirect/bfcache path the DOM
is often already fully parsed by the time generator.js runs, so
`DOMContentLoaded` has ALREADY fired. The bootstrap was registered only via
addEventListener('DOMContentLoaded', initGenerator), which then never fires —
initGenerator() never runs, so no prefill and no auto-generate. It was
intermittent because it depended on cache/redirect timing.

Fix: guard on document.readyState — attach the DOMContentLoaded listener while
still 'loading', otherwise call initGenerator() immediately since the DOM is
already available. The bootstrap is additionally gated behind
`typeof module === 'undefined'` so requiring the file in the (DOM-less) test
runner to exercise the pure helpers stays a no-op.

The 404.html half was already correct (verified live: it is served for
/<username>, parses + stashes the username, and redirects to /).

Tests: adds tests/generator-bootstrap.spec.js, which executes the real browser
path against jsdom and asserts the deep-link username is prefilled when
readyState is 'complete'/'interactive' (the regressing case) and still deferred
correctly when 'loading'. Verified the new spec FAILS on the pre-fix code and
passes after. Full suite green (137 passing, was 133).

No public API / widget behavior change.

* fix(lint): resolve ESLint error in bootstrap test + tidy catch bindings

CI (ESLint) failed on tests/generator-bootstrap.spec.js: an unused catch
binding (`catch (e)`) is an error under the recommended config for tests/**
(which, unlike js/**, has no no-unused-vars override).

- Use optional catch binding (`catch {}`) in the new bootstrap test and in
  the two deep-link helper catches in js/generator.js (were warnings).
- Drop three now-unnecessary `eslint-disable no-new-func` directives that
  ESLint flagged as unused (no-new-func isn't enabled), in the bootstrap and
  parity specs.

eslint . -> 0 errors (remaining warnings are pre-existing, unrelated).
npm test green (137 passing).
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.

2 participants