Skip to content

Instantly share code, notes, and snippets.

@danielsimao
Created March 27, 2026 11:16
Show Gist options
  • Select an option

  • Save danielsimao/7513d9d24b1f84d2d0d7df8dcc3f5b13 to your computer and use it in GitHub Desktop.

Select an option

Save danielsimao/7513d9d24b1f84d2d0d7df8dcc3f5b13 to your computer and use it in GitHub Desktop.
BOB UI — QR Code Fallback Wireframes (walletless deposit)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QR Code Fallback — Wireframe Concepts</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,500;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;600&display=swap');
:root {
--bg: #09090b;
--surface: #111113;
--surface-2: #1a1a1e;
--border: #27272a;
--muted: #71717a;
--text: #fafafa;
--text-dim: #a1a1aa;
--orange: #f97316;
--orange-dim: rgba(249,115,22,0.15);
--green: #22c55e;
--warning: #eab308;
--destructive: #ef4444;
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'DM Sans', system-ui, sans-serif;
line-height: 1.6;
padding: 40px 24px 80px;
max-width: 1100px;
margin: 0 auto;
}
code, .mono { font-family: 'JetBrains Mono', monospace; }
/* Header */
.header { margin-bottom: 48px; }
.header h1 { font-size: 32px; font-weight: 700; margin-bottom: 8px; letter-spacing: -0.5px; }
.header .subtitle { color: var(--muted); font-size: 14px; line-height: 1.7; max-width: 700px; }
.header .tag { display: inline-block; background: var(--orange-dim); color: var(--orange); font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 20px; margin-bottom: 12px; letter-spacing: 0.5px; text-transform: uppercase; }
/* Problem section */
.problem { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin-bottom: 48px; }
.problem h2 { font-size: 14px; color: var(--orange); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; font-weight: 600; }
.problem .current { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.problem .current-code { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; font-size: 12px; color: var(--text-dim); flex: 1; min-width: 280px; }
.problem .current-code strong { color: var(--text); }
.problem .issues { flex: 1; min-width: 280px; }
.problem .issues li { color: var(--text-dim); font-size: 13px; margin-bottom: 6px; padding-left: 4px; }
.problem .issues li::marker { color: var(--destructive); }
/* Concept cards */
.concepts { display: flex; flex-direction: column; gap: 32px; margin-bottom: 48px; }
.concept { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; overflow: hidden; }
.concept-header { padding: 24px 28px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.concept-header h2 { font-size: 20px; font-weight: 700; letter-spacing: -0.3px; }
.concept-header .bet { color: var(--text-dim); font-size: 13px; margin-top: 4px; max-width: 500px; }
.concept-header .badge { display: inline-flex; align-items: center; gap: 4px; background: var(--orange-dim); color: var(--orange); font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: 16px; white-space: nowrap; height: fit-content; margin-top: 4px; }
.concept-body { display: flex; gap: 0; flex-wrap: wrap; }
.concept-sketch { flex: 1; min-width: 300px; padding: 24px 28px; border-right: 1px solid var(--border); }
.concept-detail { flex: 1; min-width: 300px; padding: 24px 28px; }
.concept-sketch h3, .concept-detail h3 { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 16px; font-weight: 600; }
/* Mock dialog */
.mock-dialog { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-size: 12px; }
.mock-dialog .label { color: var(--muted); font-size: 9px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; font-weight: 700; }
.mock-qr { width: 100px; height: 100px; background: white; border-radius: 8px; margin: 8px auto; display: flex; align-items: center; justify-content: center; }
.mock-qr-inner { width: 70px; height: 70px; background: repeating-conic-gradient(var(--bg) 0% 25%, white 0% 50%) 0 0 / 10px 10px; border-radius: 2px; }
.mock-address { background: var(--surface-2); border-radius: 6px; padding: 6px 8px; font-size: 10px; color: var(--text-dim); text-align: center; margin: 8px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mock-toggle { text-align: center; margin-top: 10px; }
.mock-toggle a { color: var(--orange); font-size: 10px; text-decoration: none; border-bottom: 1px dashed var(--orange); cursor: pointer; }
.mock-warning { text-align: center; font-size: 9px; color: var(--warning); opacity: 0.7; margin-top: 6px; }
.mock-tabs { display: flex; gap: 0; margin-bottom: 10px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.mock-tabs .tab { flex: 1; text-align: center; padding: 6px 8px; font-size: 10px; color: var(--muted); background: var(--surface-2); cursor: pointer; }
.mock-tabs .tab.active { background: var(--orange-dim); color: var(--orange); font-weight: 600; }
.mock-badge { display: inline-block; background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px; padding: 2px 8px; font-size: 9px; color: var(--text-dim); margin: 4px auto; }
/* Assessment */
.assessment { margin-top: 12px; }
.assessment table { width: 100%; border-collapse: collapse; font-size: 12px; }
.assessment td { padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
.assessment td:first-child { color: var(--muted); width: 140px; }
.rating { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; }
.rating-low { background: rgba(34,197,94,0.15); color: var(--green); }
.rating-med { background: rgba(234,179,8,0.15); color: var(--warning); }
.rating-high { background: rgba(239,68,68,0.15); color: var(--destructive); }
.pros-cons { display: flex; gap: 16px; margin-top: 16px; flex-wrap: wrap; }
.pros, .cons { flex: 1; min-width: 140px; }
.pros h4, .cons h4 { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; font-weight: 600; }
.pros h4 { color: var(--green); }
.cons h4 { color: var(--destructive); }
.pros li, .cons li { font-size: 12px; color: var(--text-dim); margin-bottom: 4px; padding-left: 2px; }
/* Comparison */
.comparison { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 28px; margin-bottom: 32px; }
.comparison h2 { font-size: 18px; font-weight: 700; margin-bottom: 20px; }
.comparison table { width: 100%; border-collapse: collapse; font-size: 13px; }
.comparison th { text-align: left; padding: 10px 12px; border-bottom: 2px solid var(--border); color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; }
.comparison td { padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); }
.comparison tr:last-child td { border-bottom: none; }
/* Recommendation */
.rec { background: var(--surface); border: 2px solid var(--orange); border-radius: 16px; padding: 28px; }
.rec h2 { font-size: 18px; font-weight: 700; color: var(--orange); margin-bottom: 12px; }
.rec p { color: var(--text-dim); font-size: 14px; line-height: 1.7; margin-bottom: 10px; }
.rec strong { color: var(--text); }
.reuse-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.reuse-tag { font-size: 10px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
.reuse-tag.reuse { background: rgba(34,197,94,0.15); color: var(--green); }
.reuse-tag.extend { background: rgba(234,179,8,0.15); color: var(--warning); }
.reuse-tag.new { background: rgba(249,115,22,0.15); color: var(--orange); }
@media (max-width: 700px) {
.concept-sketch { border-right: none; border-bottom: 1px solid var(--border); }
.concept-body { flex-direction: column; }
}
</style>
</head>
<body>
<div class="header">
<div class="tag">Walletless Swap UX</div>
<h1>QR Code Fallback Wireframes</h1>
<p class="subtitle">
The walletless deposit modal encodes a BIP-21 URI (<code class="mono">bitcoin:addr?amount=X</code>) in the QR code. Some wallets, CEX withdrawal pages, and hardware companion apps can't parse this — they need a raw address. These concepts explore how to offer a fallback without cluttering the primary flow.
</p>
</div>
<div class="problem">
<h2>Current Implementation</h2>
<div class="current">
<div class="current-code">
<strong>QR Value:</strong><br>
<code class="mono" style="font-size:11px; color:var(--orange);">bitcoin:bc1p3ucg...?amount=0.01</code><br><br>
<strong>Encodes:</strong> BIP-21 URI with amount pre-filled<br>
<strong>File:</strong> <code class="mono" style="font-size:10px;">deposit-info.tsx:16</code>
</div>
<ul class="issues">
<li>CEX withdrawal pages (Binance, Coinbase) only accept raw addresses</li>
<li>Ledger Live scans raw addresses, ignores URI parameters</li>
<li>iOS/Android native camera apps don't understand <code class="mono">bitcoin:</code> scheme</li>
<li>Some older wallets silently drop the <code class="mono">?amount=</code> parameter</li>
</ul>
</div>
</div>
<!-- CONCEPT A -->
<div class="concepts">
<div class="concept">
<div class="concept-header">
<div>
<h2>A: Inline Toggle Link</h2>
<p class="bet">Minimal disruption — a single text link below the QR swaps it to address-only mode. Same space, no layout shift.</p>
</div>
<div class="badge">Recommended</div>
</div>
<div class="concept-body">
<div class="concept-sketch">
<h3>Layout Sketch</h3>
<div class="mock-dialog">
<div class="label">To this address</div>
<div class="mock-qr"><div class="mock-qr-inner"></div></div>
<div class="mock-address">bc1p3ucgeerdehagm5jlwkd9qah...pwm4wqg06p2a</div>
<div class="mock-toggle"><a>Problems scanning? Show address-only QR</a></div>
<div class="mock-warning">Only send BTC on the Bitcoin network</div>
</div>
<p style="font-size:11px; color:var(--muted); margin-top:12px;">
After clicking, the link text changes to "Show full QR with amount" and the QR re-renders with just the raw address. Toggles back and forth.
</p>
</div>
<div class="concept-detail">
<h3>Assessment</h3>
<div class="assessment">
<table>
<tr><td>UX Viability</td><td><span class="rating rating-low">High</span></td></tr>
<tr><td>Market Fit</td><td><span class="rating rating-low">High</span></td></tr>
<tr><td>Implementation</td><td><span class="rating rating-low">Low</span></td></tr>
<tr><td>Integration Risk</td><td><span class="rating rating-low">Low</span></td></tr>
</table>
</div>
<h3 style="margin-top:16px">Component Reuse</h3>
<div class="reuse-tags">
<span class="reuse-tag reuse">Reuse: QRCode</span>
<span class="reuse-tag reuse">Reuse: Tooltip</span>
<span class="reuse-tag extend">Extend: DepositInfo</span>
<span class="reuse-tag new">New: toggle state (1 useState)</span>
</div>
<div class="pros-cons">
<div class="pros">
<h4>Pros</h4>
<ul>
<li>Zero layout shift — QR stays same size</li>
<li>1 new useState + conditional qrValue</li>
<li>Discoverable for users who need it</li>
<li>Invisible to users who don't</li>
</ul>
</div>
<div class="cons">
<h4>Cons</h4>
<ul>
<li>User must know they have a problem first</li>
<li>Toggle text needs translation (2 new strings)</li>
<li>No visual hint about what changed</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- CONCEPT B -->
<div class="concept">
<div class="concept-header">
<div>
<h2>B: Tabbed QR Modes</h2>
<p class="bet">Make both QR types equally accessible — tabs let the user switch between "With Amount" and "Address Only" without implying one is a fallback.</p>
</div>
</div>
<div class="concept-body">
<div class="concept-sketch">
<h3>Layout Sketch</h3>
<div class="mock-dialog">
<div class="label">To this address</div>
<div class="mock-tabs">
<div class="tab active">With Amount</div>
<div class="tab">Address Only</div>
</div>
<div class="mock-qr"><div class="mock-qr-inner"></div></div>
<div class="mock-badge">bitcoin:bc1p...?amount=0.01</div>
<div class="mock-address">bc1p3ucgeerdehagm5jlwkd9qah...pwm4wqg06p2a</div>
<div class="mock-warning">Only send BTC on the Bitcoin network</div>
</div>
<p style="font-size:11px; color:var(--muted); margin-top:12px;">
The badge below the QR shows what's encoded. "Address Only" tab shows just the raw address in the badge. Both tabs share the same copy button and address display.
</p>
</div>
<div class="concept-detail">
<h3>Assessment</h3>
<div class="assessment">
<table>
<tr><td>UX Viability</td><td><span class="rating rating-low">High</span></td></tr>
<tr><td>Market Fit</td><td><span class="rating rating-med">Medium</span></td></tr>
<tr><td>Implementation</td><td><span class="rating rating-med">Medium</span></td></tr>
<tr><td>Integration Risk</td><td><span class="rating rating-low">Low</span></td></tr>
</table>
</div>
<h3 style="margin-top:16px">Component Reuse</h3>
<div class="reuse-tags">
<span class="reuse-tag reuse">Reuse: QRCode</span>
<span class="reuse-tag reuse">Reuse: Badge</span>
<span class="reuse-tag extend">Extend: DepositInfo</span>
<span class="reuse-tag new">New: tab switcher</span>
</div>
<div class="pros-cons">
<div class="pros">
<h4>Pros</h4>
<ul>
<li>Both options equally visible — no "fallback" stigma</li>
<li>Badge clarifies what's encoded (educational)</li>
<li>Familiar tab pattern</li>
</ul>
</div>
<div class="cons">
<h4>Cons</h4>
<ul>
<li>Takes more vertical space (tab bar)</li>
<li>May confuse non-technical users ("what's the difference?")</li>
<li>More UI surface in an already dense modal</li>
<li>Overkill for a niche fallback</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- CONCEPT C -->
<div class="concept">
<div class="concept-header">
<div>
<h2>C: Default to Address-Only, Show Amount as Enhancement</h2>
<p class="bet">Flip the default — show the universally compatible raw address QR by default. Power users who want the BIP-21 URI can opt in.</p>
</div>
</div>
<div class="concept-body">
<div class="concept-sketch">
<h3>Layout Sketch</h3>
<div class="mock-dialog">
<div class="label">To this address</div>
<div class="mock-qr"><div class="mock-qr-inner"></div></div>
<div class="mock-address">bc1p3ucgeerdehagm5jlwkd9qah...pwm4wqg06p2a</div>
<div style="background:var(--surface-2); border:1px solid var(--border); border-radius:8px; padding:8px; margin-top:8px; text-align:center;">
<span style="font-size:10px; color:var(--text-dim);">Amount: <strong style="color:var(--text)">0.01 BTC</strong></span>
<span style="font-size:9px; color:var(--muted); display:block; margin-top:2px;">Enter this amount manually in your wallet</span>
</div>
<div class="mock-toggle"><a>Include amount in QR code</a></div>
<div class="mock-warning">Only send BTC on the Bitcoin network</div>
</div>
<p style="font-size:11px; color:var(--muted); margin-top:12px;">
The amount is displayed as text below the address, with a clear instruction. The BIP-21 URI is available via the toggle for wallets that support it.
</p>
</div>
<div class="concept-detail">
<h3>Assessment</h3>
<div class="assessment">
<table>
<tr><td>UX Viability</td><td><span class="rating rating-low">High</span></td></tr>
<tr><td>Market Fit</td><td><span class="rating rating-low">High</span></td></tr>
<tr><td>Implementation</td><td><span class="rating rating-low">Low</span></td></tr>
<tr><td>Integration Risk</td><td><span class="rating rating-med">Medium</span></td></tr>
</table>
</div>
<h3 style="margin-top:16px">Component Reuse</h3>
<div class="reuse-tags">
<span class="reuse-tag reuse">Reuse: QRCode</span>
<span class="reuse-tag extend">Extend: DepositInfo</span>
<span class="reuse-tag new">New: amount display block</span>
</div>
<div class="pros-cons">
<div class="pros">
<h4>Pros</h4>
<ul>
<li>Works with every wallet, CEX, and camera app out of the box</li>
<li>Amount displayed as text — user always knows how much to send</li>
<li>No confusion about "problems scanning"</li>
<li>BIP-21 still available for power users</li>
</ul>
</div>
<div class="cons">
<h4>Cons</h4>
<ul>
<li>Users with BIP-21 wallets must enter amount manually (or toggle)</li>
<li>Risk of sending wrong amount if user ignores the text</li>
<li>Changes the default behavior — breaking change for existing users</li>
<li>Slightly more vertical space for the amount block</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Comparison -->
<div class="comparison">
<h2>Comparison</h2>
<table>
<thead>
<tr>
<th></th>
<th>A: Inline Toggle</th>
<th>B: Tabbed Modes</th>
<th>C: Address Default</th>
</tr>
</thead>
<tbody>
<tr>
<td style="color:var(--muted)">UX Viability</td>
<td><span class="rating rating-low">High</span></td>
<td><span class="rating rating-low">High</span></td>
<td><span class="rating rating-low">High</span></td>
</tr>
<tr>
<td style="color:var(--muted)">Market Fit</td>
<td><span class="rating rating-low">High</span></td>
<td><span class="rating rating-med">Medium</span></td>
<td><span class="rating rating-low">High</span></td>
</tr>
<tr>
<td style="color:var(--muted)">Implementation</td>
<td><span class="rating rating-low">Low</span></td>
<td><span class="rating rating-med">Medium</span></td>
<td><span class="rating rating-low">Low</span></td>
</tr>
<tr>
<td style="color:var(--muted)">Integration Risk</td>
<td><span class="rating rating-low">Low</span></td>
<td><span class="rating rating-low">Low</span></td>
<td><span class="rating rating-med">Medium</span></td>
</tr>
<tr>
<td style="color:var(--muted)">Component Reuse</td>
<td>Extend DepositInfo only</td>
<td>Extend + new tab bar</td>
<td>Extend + new amount block</td>
</tr>
<tr>
<td style="color:var(--muted)">New i18n strings</td>
<td>2</td>
<td>3</td>
<td>3</td>
</tr>
</tbody>
</table>
</div>
<!-- Recommendation -->
<div class="rec">
<h2>Recommendation: Concept A (Inline Toggle)</h2>
<p>
<strong>Concept A</strong> is the right choice because it solves the problem with zero disruption to the 80%+ of users whose wallets handle BIP-21 fine. The toggle link is invisible until needed, adds one `useState` to `DepositInfo`, and requires no new components — just a conditional on the existing `qrValue` variable.
</p>
<p>
<strong>Runner-up: Concept C</strong> — if analytics show a high rate of failed/incomplete deposits (users scanning but sending wrong amounts or not sending at all), flipping the default to address-only would be the safer long-term move. But it's a bigger behavioral change that should be data-driven, not preemptive.
</p>
<p>
<strong>What to validate first:</strong> Track how many users click "Problems scanning?" — if the rate is high (>10%), consider switching to Concept C as the default.
</p>
</div>
<p style="margin-top:32px; text-align:center; color:var(--muted); font-size:11px;">
BOB UI &mdash; QR Code Fallback Wireframes &mdash; Generated for walletless swap deposit flow
</p>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment