From aced21ab434564a9adfd12d18cbabe9f7fa1840e Mon Sep 17 00:00:00 2001 From: def Date: Sun, 15 Mar 2026 19:55:45 +0000 Subject: [PATCH] Improve portal crypto wallet UX with mobile deeplink and copy fallback --- templates/portal_invoice_detail.html | 162 +++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 12 deletions(-) diff --git a/templates/portal_invoice_detail.html b/templates/portal_invoice_detail.html index dd4e22a..f907265 100644 --- a/templates/portal_invoice_detail.html +++ b/templates/portal_invoice_detail.html @@ -26,16 +26,42 @@ .pay-selector { padding: 10px 12px; min-width: 220px; border-radius: 8px; } .pay-panel { margin-top: 1rem; padding: 1rem; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; background: rgba(255,255,255,0.02); } .pay-panel.hidden { display: none; } - .pay-btn { display:inline-block; padding:12px 18px; color:#ffffff; text-decoration:none; border-radius:8px; font-weight:700; border:none; cursor:pointer; margin:8px 0 0 0; } + + .pay-btn { + display:inline-block; + padding:12px 18px; + color:#ffffff; + text-decoration:none; + border-radius:8px; + font-weight:700; + border:none; + cursor:pointer; + margin:8px 0 0 0; + } .pay-btn-square { background:#16a34a; } .pay-btn-wallet { background:#2563eb; } + .pay-btn-mobile { background:#7c3aed; } + .pay-btn-copy { background:#374151; } + .error-box { border: 1px solid rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); color: #fecaca; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; } .success-box { border: 1px solid rgba(34, 197, 94, 0.55); background: rgba(22, 101, 52, 0.18); color: #dcfce7; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; } .snapshot-wrap { position: relative; margin-top: 1rem; border: 1px solid rgba(255,255,255,0.14); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.02); } .snapshot-header { display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; } .snapshot-meta { flex: 1 1 auto; min-width: 0; line-height: 1.65; } - .snapshot-timer-box { width: 220px; min-height: 132px; border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; background: rgba(0,0,0,0.18); display:flex; flex-direction:column; justify-content:center; align-items:center; text-align:center; padding: 0.9rem; } + .snapshot-timer-box { + width: 220px; + min-height: 132px; + border: 1px solid rgba(255,255,255,0.16); + border-radius: 14px; + background: rgba(0,0,0,0.18); + display:flex; + flex-direction:column; + justify-content:center; + align-items:center; + text-align:center; + padding: 0.9rem; + } .snapshot-timer-value { font-size: 2rem; font-weight: 800; line-height: 1.1; } .snapshot-timer-label { margin-top: 0.55rem; font-size: 0.95rem; opacity: 0.95; } .snapshot-timer-expired { color: #f87171; } @@ -53,9 +79,46 @@ .lock-box.expired { border-color: rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); } .lock-grid { display:grid; grid-template-columns: 1fr 220px; gap:1rem; align-items:start; } .lock-code { display:block; margin-top:0.35rem; padding:0.65rem 0.8rem; background: rgba(0,0,0,0.22); border-radius: 8px; overflow-wrap:anywhere; } - .wallet-actions { display:flex; gap:0.75rem; flex-wrap:wrap; margin-top:0.9rem; align-items:center; } + + .wallet-actions { + display:flex; + gap:0.75rem; + flex-wrap:wrap; + margin-top:0.9rem; + align-items:center; + } + .wallet-help { + margin-top: 0.85rem; + padding: 0.9rem 1rem; + border-radius: 10px; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.10); + } + .wallet-help h4 { + margin: 0 0 0.55rem 0; + font-size: 1rem; + } + .wallet-help p { + margin: 0.35rem 0; + } .wallet-note { opacity:0.9; margin-top:0.65rem; } .mono { font-family: monospace; } + .copy-row { + display:flex; + gap:0.5rem; + flex-wrap:wrap; + align-items:center; + margin-top:0.65rem; + } + .copy-target { + flex: 1 1 420px; + min-width: 220px; + } + .copy-status { + display:inline-block; + margin-left: 0.5rem; + opacity: 0.9; + } @media (max-width: 820px) { .snapshot-header, .lock-grid { grid-template-columns: 1fr; display:block; } @@ -147,7 +210,7 @@
Source Status: {{ invoice.oracle_quote.source_status or "—" }}
Frozen Amount: {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}
{% if pending_crypto_payment %} -
Price locked for 2 minutes after acceptance.
+
Your quote is protected after acceptance.
{% else %}
Select a crypto asset to accept the quote.
{% endif %} @@ -161,7 +224,7 @@ {% elif pending_crypto_payment %}
--:--
-
This price is locked for 2 minutes
+
Quote protected while you open wallet
{% else %}
@@ -178,9 +241,9 @@

{{ selected_crypto_option.label }} Payment Instructions

Send exactly: {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}
Destination wallet:
- {{ pending_crypto_payment.wallet_address }} + {{ pending_crypto_payment.wallet_address }}
Reference / Invoice:
- {{ pending_crypto_payment.reference }} + {{ pending_crypto_payment.reference }} {% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
@@ -198,13 +261,41 @@ data-decimals="{{ selected_crypto_option.decimals }}" data-token-contract="{{ selected_crypto_option.token_contract or '' }}" > - Pay with MetaMask / Rabby + Open MetaMask / Rabby - + + + Open in MetaMask Mobile + + + +
+ +
+

Fastest way to pay

+

1. Click Open MetaMask / Rabby if your wallet is installed in this browser.

+

2. If that does not open your wallet, click Open in MetaMask Mobile.

+

3. If needed, use Copy Payment Details and send manually.

+
- This will open your wallet, prepare the exact transaction, and submit the tx hash back to the portal automatically. + You do not need to finish everything inside the short quote timer. Once accepted, the quote is protected while you open your wallet.
+ +
+ + +
+ {% elif pending_crypto_payment.txid %}
Transaction Hash:
{{ pending_crypto_payment.txid }} @@ -222,7 +313,7 @@ {% else %}
--:--
-
This price is locked for 2 minutes
+
Quote protected while you open wallet
{% endif %}
@@ -376,6 +467,53 @@ }); } + function buildMetaMaskMobileLink() { + const currentUrl = window.location.href; + return "https://link.metamask.io/dapp/" + currentUrl.replace(/^https?:\/\//, ""); + } + + const mmLink = document.getElementById("metamaskMobileLink"); + if (mmLink) { + mmLink.href = buildMetaMaskMobileLink(); + } + + async function copyText(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + return; + } + const ta = document.createElement("textarea"); + ta.value = text; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + ta.remove(); + } + + const copyBtn = document.getElementById("copyDetailsButton"); + if (copyBtn) { + copyBtn.addEventListener("click", async function() { + const copyStatus = document.getElementById("copyStatusText"); + const walletAddress = document.getElementById("walletAddressText")?.textContent || ""; + const invoiceRef = document.getElementById("invoiceRefText")?.textContent || ""; + const amount = document.getElementById("walletPayButton")?.dataset.amount || ""; + const asset = document.getElementById("walletPayButton")?.dataset.asset || ""; + + const payload = +`Asset: ${asset} +Amount: ${amount} ${asset} +Wallet: ${walletAddress} +Reference: ${invoiceRef}`; + + try { + await copyText(payload); + if (copyStatus) copyStatus.textContent = "Payment details copied."; + } catch (err) { + if (copyStatus) copyStatus.textContent = "Copy failed."; + } + }); + } + const walletButton = document.getElementById("walletPayButton"); if (walletButton) { walletButton.addEventListener("click", async function() { @@ -386,7 +524,7 @@ try { if (!window.ethereum) { - throw new Error("No wallet detected. Open this page in MetaMask or Rabby."); + throw new Error("No wallet detected in this browser. Use 'Open in MetaMask Mobile' or copy the payment details."); } const invoiceId = walletButton.dataset.invoiceId;