Skip to content

feat: share an invoice by link or QR code, and import it - #209

Draft
Atharva0506 wants to merge 8 commits into
StabilityNexus:mainfrom
Atharva0506:feat/invoice-share-link-qr
Draft

feat: share an invoice by link or QR code, and import it#209
Atharva0506 wants to merge 8 commits into
StabilityNexus:mainfrom
Atharva0506:feat/invoice-share-link-qr

Conversation

@Atharva0506

@Atharva0506 Atharva0506 commented Sep 3, 2026

Copy link
Copy Markdown
Member

Addressed Issues:

issue number: #206

Relay delivery cannot start until the recipient has registered a messaging key on-chain. Until they do, the invoice exists on-chain but its payload has nowhere to go, and the recipient sees only the on-chain stub — no names, no line items, no dates. This adds a share link, QR code and file that carry the payload themselves, so sharing works with no key registered and with the relay down.

Screenshots/Recordings:

freecompress-qr-link-chainvoice.mp4

Additional Notes:

  • Not encrypted, deliberately. The only key that could travel with a self-contained link is one embedded in the same link, which anyone holding the link also holds. That is obfuscation, not confidentiality, so the dialog says plainly that anyone with the link can view the invoice and that it cannot be revoked. A passphrase mode is the honest way to add privacy later.
  • Integrity comes from the chain, not the token. The payload was committed on-chain as invoiceDataHash at creation, so the importer recomputes that hash and compares — the same check storeRelayInvoice already makes. Stronger than a signature the token carries about itself, and a tampered link is refused.
  • The token goes in the hash fragment, never a query string, so invoice details never reach an access log, CDN cache or Referer header.
  • Import works before the wallet connects. Decoding needs no wallet and verification reads a public RPC, so a tapped link renders the real invoice immediately; connecting is only needed to save or pay. Verified payloads land in the same IndexedDB store the relay writes to, so the existing list pages pick them up unchanged.
  • Size. Deflate + base64url puts a typical invoice around 550–650 characters. The raw URL is never the interface — Copy, navigator.share, a QR code and a .cvinv file are — and the dialog falls back to the file past what a QR or chat client will carry.
  • Importing also recovers a sender's own invoice on a new device, since the payload has only ever lived in local storage.
  • Two deps: fflate (~8 KB) and qrcode, which was already in the tree transitively via RainbowKit.

26 new tests covering the round trip, hash preservation, tamper and truncation rejection, version rejection and both verification branches. 185 total pass; lint and build clean.

Left as follow-ups to keep this reviewable: offering the link automatically on the "client hasn't registered" branch in CreateInvoice, forwarding from ReceivedInvoice, a wallet-free public read-only view, relay-backed short links.

Builds on #205. Draft until #205 is reviewed and merged, then rebased onto main.

AI Usage Disclosure:

Check one of the checkboxes below:

  • This PR does not contain AI-generated code at all.
  • This PR contains AI-generated code. I have read the AI Usage Policy and this PR complies with this policy. I have tested the code locally and I am responsible for it.

I have used the following AI models and tools: Claude Code (CLI), model Claude Opus 5

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
  • I have filled this PR template completely and carefully, and I understand that my PR may be closed without review otherwise.

The dashboard nested four levels of horizontal padding and then applied an
inner max-w-7xl on top of a fixed sidebar, which left a wide dead gutter on
the right and pushed the invoice items well below the fold.

- Add src/utils/layout.js holding the page container, card, header and
  section-gap classes so the dashboard rhythm is tuned in one place instead
  of per-page magic classes.
- Drop the inner max-width from every dashboard page and trim the padding
  and section margins across Create Invoice, Create Multiple Invoices, Sent,
  Received, Request Invoices and Settings.
- Replace the mobile MUI Select with a hamburger that opens a slide-out
  Drawer. The nav list is extracted into DashboardNav and rendered by both
  the desktop rail and the drawer, so the two cannot drift apart. The
  hamburger doubles as the current-section label, and navigating closes the
  drawer instead of leaving the overlay parked over the new page.
- Fix the fixed-navbar offset: the navbar is h-24 but the main padding was
  pt-20, so content sat 16px underneath and only cleared it thanks to an
  unrelated mt-4 on the dashboard heading.
Removing the inner max-width fixed the dead right gutter but left the page
uncapped, which read as stretched. The cause turned out to be alignment
rather than width: the invoice meta bar stopped well short of the items
table below it, so the right edge was ragged.

- Share one gutter (SHELL) between the navbar and every page shell, so the
  logo, the wallet button and the page content sit on the same left and
  right edges. Treasure adopts it too.
- Give the meta bar, the Client/Payment row, the items table and the total
  row the same left and right edges, so the layout reads as deliberate at
  any width. Cards in the row stretch to equal height.
- Collapse the desktop rail to a 68px icon-only strip with tooltips; the
  choice persists in localStorage, read synchronously so the rail does not
  render expanded and then snap shut.
- Replace the sidebar greeting's empty space with the connected address and
  network, keeping the greeting above it.
- Cap the invoice total at a min-width instead of a third of the row, where
  the label and amount drifted apart on a wide screen.
- Settings: let each card own its heading rather than repeating the same
  title in the section above it.
The sidebar header stacked three lines — greeting, address, network — and the
address and network were already shown in the navbar, so two of them were
duplication. Drop them and pair "Welcome Back!" with the collapse toggle on a
single row, which reclaims about 40px above the nav items.
Addresses CodeRabbit review on StabilityNexus#205.

- The drawer was hidden by MUI's lg (1200px) while the hamburger and rail
  use Tailwind's (1024px), so between the two the drawer stayed mounted with
  nothing visible to close it. It is now keyed to 1024px like the rest, and
  closes when the viewport crosses that width so the modal cannot leave the
  body scroll locked. The hamburger gets aria-controls for the drawer.
- toggleRail wrote to localStorage inside the state updater. Updaters are
  expected to be pure and React may call them more than once per dispatch,
  so the write now happens before setState.
- SHELL dropped its max-width earlier but the comments still described a
  width cap, and mx-auto had nothing to centre once w-full was the only
  width. Reworded and removed.
Relay delivery cannot start until the recipient has registered a
messaging key on-chain. Until they do, the invoice exists on-chain but
its payload has nowhere to go and the recipient sees only the on-chain
stub. A share token carries the payload itself, so it works with no key
registered, and with the relay down or the message expired.

The token is deliberately not encrypted. The only key that could travel
with a self-contained link is one embedded in the same link, which
anybody holding the link also holds — that is obfuscation, not
confidentiality, and calling it encryption would misrepresent it.

Integrity is not left to the token either. The payload was committed
on-chain as invoiceDataHash when the invoice was created, so the
importer recomputes that hash and compares it against the chain. That is
strictly stronger than any signature the token could carry about itself,
and it needs no wallet: every supported chain ships a public RPC URL.
Adds a Share entry to the drawer's export menu, alongside PDF, CSV and
JSON.

The dialog reads the payload from IndexedDB rather than taking it from
the invoice object the page is rendering. Those are not the same thing:
the list pages enrich the stored payload with token logos, decimals and
status before rendering, and any of those additions would change the
hash and make the token unverifiable on the recipient's side.

Length stops mattering because the raw URL is never the interface —
Copy, the OS share sheet, a QR code and a file are, and the link itself
shows only middle-truncated. A typical invoice lands around 550
characters; the dialog falls back to the file when one grows past what a
QR code or a chat client will carry.

Outline buttons carry an explicit text colour here: the variant sets a
background but no foreground, so on these white cards they would
otherwise inherit the dark shell's white and render invisible.
A tapped link or scanned QR lands on /dashboard/import with the token in
the hash fragment, which is never sent to a server — so the invoice
details stay out of access logs, CDN caches and Referer headers.

The page works before the wallet connects: decoding needs no wallet and
verification reads the chain over a public RPC, so someone opening a
link sees the real invoice immediately rather than a connect prompt in
front of a blank page. Connecting is needed only to save it.

Nothing is saved on the strength of the link alone. The payload is
trusted only once its hash matches the on-chain commitment, and it is
then written to the same IndexedDB store the relay writes to — so the
existing list pages pick it up with no changes.

Importing also recovers a sender's own invoice on a new device, since
the payload has only ever lived in local storage.
…drawer

Two problems, both reported from testing.

Sharing was hidden inside the Export Invoice menu. It is not an export —
it is how you get the invoice to someone — and burying it there cost it
its discoverability. It now has a share icon in each row's actions, next
to resend and view, and a Share button of its own in the drawer footer.
Both appear only when the payload is on this device, matching resend:
without it there is nothing to put in a link.

The dialog was also unreachable. The shadcn dialog and the MUI
SwipeableDrawer are separate portal stacks — the drawer sits at z-index
1200 and the dialog at 50 — so the drawer painted over the dialog and
clipped its right-hand buttons, Share among them. Rebuilding the dialog
on MUI's Dialog puts both under one modal manager, which stacks them
correctly, and matches the drawer and confirm dialog already on this
page. Verified with hit-testing: every button in the dialog is now the
topmost element at its own centre.
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Sep 3, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​qrcode@​1.5.410010010082100

View full report

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Build successful!

Build Size Metrics
Metrics not found

Workflow run · commit db53969

@Atharva0506
Atharva0506 force-pushed the feat/invoice-share-link-qr branch from cfdb4b1 to db53969 Compare September 4, 2026 17:37
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🔍 PR Preview

Preview https://chainvoice.stability.nexus/pr-preview/pr-209/
Commit db53969
Networks Ethereum Sepolia only

Rebuilt on every commit, and removed when this pull request closes.

github-actions Bot added a commit that referenced this pull request Sep 4, 2026
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