Created
March 27, 2026 11:16
-
-
Save danielsimao/7513d9d24b1f84d2d0d7df8dcc3f5b13 to your computer and use it in GitHub Desktop.
BOB UI — QR Code Fallback Wireframes (walletless deposit)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 — QR Code Fallback Wireframes — 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