@ -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 @@
< div > < strong > Source Status:< / strong > {{ invoice.oracle_quote.source_status or "—" }}< / div >
< div > < strong > Frozen Amount:< / strong > {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}< / div >
{% if pending_crypto_payment %}
< div style = "margin-top:0.75rem;" > < strong > Price locked for 2 minutes after acceptance.< / strong > < / div >
< div style = "margin-top:0.75rem;" > < strong > Your quote is protected after acceptance.< / strong > < / div >
{% else %}
< div style = "margin-top:0.75rem;" > < strong > Select a crypto asset to accept the quote.< / strong > < / div >
{% endif %}
@ -161,7 +224,7 @@
{% elif pending_crypto_payment %}
< div class = "snapshot-timer-box" >
< div id = "lockTimerValue" class = "snapshot-timer-value" data-expiry = "{{ pending_crypto_payment.lock_expires_at_iso }}" > --:--< / div >
< div id = "lockTimerLabel" class = "snapshot-timer-label" > This price is locked for 2 minutes < / div >
< div id = "lockTimerLabel" class = "snapshot-timer-label" > Quote protected while you open wallet < / div >
< / div >
{% else %}
< div class = "snapshot-timer-box" >
@ -178,9 +241,9 @@
< h3 style = "margin-top:0;" > {{ selected_crypto_option.label }} Payment Instructions< / h3 >
< div > < strong > Send exactly:< / strong > {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}< / div >
< div style = "margin-top:0.65rem;" > < strong > Destination wallet:< / strong > < / div >
< code class = "lock-code" > {{ pending_crypto_payment.wallet_address }}< / code >
< code id = "walletAddressText" class = "lock-code copy-target " > {{ pending_crypto_payment.wallet_address }}< / code >
< div style = "margin-top:0.65rem;" > < strong > Reference / Invoice:< / strong > < / div >
< code class = "lock-code" > {{ pending_crypto_payment.reference }}< / code >
< code id = "invoiceRefText" class = "lock-code copy-target " > {{ pending_crypto_payment.reference }}< / code >
{% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
< div class = "wallet-actions" >
@ -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
< / button >
< span id = "walletStatusText" > < / span >
< a
id="metamaskMobileLink"
href="#"
target="_blank"
rel="noopener noreferrer"
class="pay-btn pay-btn-mobile"
data-invoice-id="{{ invoice.id }}"
>
Open in MetaMask Mobile
< / a >
< button type = "button" id = "copyDetailsButton" class = "pay-btn pay-btn-copy" >
Copy Payment Details
< / button >
< / div >
< div class = "wallet-help" >
< h4 > Fastest way to pay< / h4 >
< p > 1. Click < strong > Open MetaMask / Rabby< / strong > if your wallet is installed in this browser.< / p >
< p > 2. If that does not open your wallet, click < strong > Open in MetaMask Mobile< / strong > .< / p >
< p > 3. If needed, use < strong > Copy Payment Details< / strong > and send manually.< / p >
< / div >
< div class = "wallet-note" >
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 .
< / div >
< div class = "copy-row" >
< span id = "walletStatusText" > < / span >
< span id = "copyStatusText" class = "copy-status" > < / span >
< / div >
{% elif pending_crypto_payment.txid %}
< div style = "margin-top:0.9rem;" > < strong > Transaction Hash:< / strong > < / div >
< code class = "lock-code mono" > {{ pending_crypto_payment.txid }}< / code >
@ -222,7 +313,7 @@
{% else %}
< div class = "snapshot-timer-box" >
< div id = "lockTimerSideValue" class = "snapshot-timer-value" data-expiry = "{{ pending_crypto_payment.lock_expires_at_iso }}" > --:--< / div >
< div id = "lockTimerSideLabel" class = "snapshot-timer-label" > This price is locked for 2 minutes < / div >
< div id = "lockTimerSideLabel" class = "snapshot-timer-label" > Quote protected while you open wallet < / div >
< / div >
{% endif %}
< / div >
@ -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;