diff --git a/templates/portal_invoice_detail.html b/templates/portal_invoice_detail.html
index 4fae514..490ca78 100644
--- a/templates/portal_invoice_detail.html
+++ b/templates/portal_invoice_detail.html
@@ -517,108 +517,155 @@ Reference: ${invoiceRef}`;
}
const walletButton = document.getElementById("walletPayButton");
+
+ function pendingTxStorageKey(invoiceId, paymentId) {
+ return `otb_pending_tx_${invoiceId}_${paymentId}`;
+ }
+
+ async function submitTxHash(invoiceId, paymentId, asset, txHash) {
+ const res = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+ },
+ body: JSON.stringify({
+ payment_id: paymentId,
+ asset: asset,
+ tx_hash: txHash
+ })
+ });
+
+ let data = {};
+ try {
+ data = await res.json();
+ } catch (err) {
+ data = { ok: false, error: "invalid_json_response" };
+ }
+
+ if (!res.ok || !data.ok) {
+ throw new Error(data.error || `submit_failed_http_${res.status}`);
+ }
+
+ return data;
+ }
+
+ async function tryRecoverPendingTxFromStorage() {
+ const invoiceId = "{{ invoice.id }}";
+ const paymentId = "{{ pending_crypto_payment.id if pending_crypto_payment else '' }}";
+ const asset = "{{ selected_crypto_option.symbol if selected_crypto_option else '' }}";
+
+ if (!invoiceId || !paymentId || !asset) return;
+ {% if pending_crypto_payment and pending_crypto_payment.txid %}
+ return;
+ {% endif %}
+
+ const key = pendingTxStorageKey(invoiceId, paymentId);
+ const savedTx = localStorage.getItem(key);
+ if (!savedTx || !savedTx.startsWith("0x")) return;
+
+ const walletStatus = document.getElementById("walletStatusText");
+ try {
+ if (walletStatus) walletStatus.textContent = "Retrying saved transaction submission...";
+ await submitTxHash(invoiceId, paymentId, asset, savedTx);
+ localStorage.removeItem(key);
+ const url = new URL(window.location.href);
+ url.searchParams.set("pay", "crypto");
+ url.searchParams.set("asset", asset);
+ url.searchParams.set("payment_id", paymentId);
+ window.location.href = url.toString();
+ } catch (err) {
+ if (walletStatus) walletStatus.textContent = `Saved tx retry failed: ${err.message}`;
+ }
+ }
+
if (walletButton) {
walletButton.addEventListener("click", async function() {
- const statusEl = document.getElementById("walletStatusText");
- const originalText = walletButton.textContent;
- walletButton.disabled = true;
- if (statusEl) statusEl.textContent = "Opening wallet…";
+ const walletStatus = document.getElementById("walletStatusText");
+ const invoiceId = this.dataset.invoiceId;
+ const paymentId = this.dataset.paymentId;
+ const asset = this.dataset.asset;
+ const chainId = this.dataset.chainId;
+ const assetType = this.dataset.assetType;
+ const to = this.dataset.to;
+ const amount = this.dataset.amount;
+ const decimals = Number(this.dataset.decimals || "18");
+ const tokenContract = this.dataset.tokenContract || "";
+
+ const setStatus = (msg) => {
+ if (walletStatus) walletStatus.textContent = msg;
+ };
+
+ if (!window.ethereum || !window.ethereum.request) {
+ setStatus("No browser wallet detected. Use MetaMask/Rabby or MetaMask Mobile.");
+ return;
+ }
try {
- if (!window.ethereum) {
- throw new Error("No wallet detected in this browser. Use 'Open in MetaMask Mobile' or copy the payment details.");
- }
-
- const invoiceId = walletButton.dataset.invoiceId;
- const paymentId = walletButton.dataset.paymentId;
- const asset = walletButton.dataset.asset;
- const chainId = walletButton.dataset.chainId;
- const assetType = walletButton.dataset.assetType;
- const to = walletButton.dataset.to;
- const amount = walletButton.dataset.amount;
- const decimals = Number(walletButton.dataset.decimals || "18");
- const tokenContract = walletButton.dataset.tokenContract || "";
-
- const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
- const from = Array.isArray(accounts) && accounts.length ? accounts[0] : "";
- if (!from) {
- throw new Error("Wallet did not return an account address.");
+ this.disabled = true;
+ setStatus("Opening wallet...");
+
+ await window.ethereum.request({ method: "eth_requestAccounts" });
+
+ if (chainId && chainId !== "None" && chainId !== "") {
+ try {
+ await switchChain(Number(chainId));
+ } catch (err) {
+ setStatus(`Chain switch failed: ${err.message || err}`);
+ this.disabled = false;
+ return;
+ }
}
- await switchChain(chainId);
-
- let txHash;
- if (assetType === "native") {
- txHash = await window.ethereum.request({
- method: "eth_sendTransaction",
- params: [{
- from: from,
- to: to,
- value: toHexBigIntFromDecimal(amount, decimals)
- }]
- });
+ let txParams;
+ if (assetType === "token" && tokenContract) {
+ txParams = {
+ from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
+ to: tokenContract,
+ data: erc20TransferData(to, amount, decimals),
+ value: "0x0"
+ };
} else {
- txHash = await window.ethereum.request({
- method: "eth_sendTransaction",
- params: [{
- from: from,
- to: tokenContract,
- data: erc20TransferData(to, amount, decimals)
- }]
- });
+ txParams = {
+ from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
+ to: to,
+ value: toHexBigIntFromDecimal(amount, decimals)
+ };
}
- if (statusEl) statusEl.textContent = "Submitting transaction hash to portal…";
-
- async function submitTxHashWithRetry() {
- const maxAttempts = 12; // about 60 seconds total
- const waitMs = 5000;
+ setStatus("Waiting for wallet confirmation...");
+ const txHash = await window.ethereum.request({
+ method: "eth_sendTransaction",
+ params: [txParams]
+ });
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
- if (statusEl) {
- statusEl.textContent = `Checking RPC for transaction… attempt ${attempt}/${maxAttempts}`;
- }
-
- const resp = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- payment_id: paymentId,
- asset: asset,
- tx_hash: txHash
- })
- });
-
- const data = await resp.json();
+ if (!txHash || !String(txHash).startsWith("0x")) {
+ throw new Error("wallet did not return a tx hash");
+ }
- if (resp.ok && data.ok) {
- window.location.href = data.redirect_url;
- return;
- }
+ const storageKey = pendingTxStorageKey(invoiceId, paymentId);
+ localStorage.setItem(storageKey, String(txHash));
- const detail = String((data && (data.detail || data.error)) || "");
- const retryable = detail.toLowerCase().includes("not found on rpc");
+ setStatus(`Wallet submitted tx: ${txHash}. Sending to billing server...`);
- if (!retryable || attempt === maxAttempts) {
- throw new Error(detail || "Portal rejected tx hash");
- }
+ await submitTxHash(invoiceId, paymentId, asset, txHash);
- if (statusEl) {
- statusEl.textContent = `Transaction sent. Waiting for RPC to see it… retrying in ${Math.floor(waitMs / 1000)}s`;
- }
+ localStorage.removeItem(storageKey);
- await new Promise(resolve => setTimeout(resolve, waitMs));
- }
- }
-
- await submitTxHashWithRetry();
+ setStatus("Transaction submitted. Reloading into processing view...");
+ const url = new URL(window.location.href);
+ url.searchParams.set("pay", "crypto");
+ url.searchParams.set("asset", asset);
+ url.searchParams.set("payment_id", paymentId);
+ window.location.href = url.toString();
} catch (err) {
- if (statusEl) statusEl.textContent = String(err.message || err);
- walletButton.disabled = false;
- walletButton.textContent = originalText;
+ setStatus(`Wallet submit failed: ${err.message || err}`);
+ this.disabled = false;
}
});
}
+
+ tryRecoverPendingTxFromStorage();
})();