mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
@@ -4,14 +4,11 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightning Tip Jar</title>
|
<title>Lightning Tip Jar</title>
|
||||||
|
|
||||||
<!-- TailwindCSS (Play CDN) -->
|
<!-- TailwindCSS (Play CDN) -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
<!-- QRCode generator -->
|
<!-- QRCode generator -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-Rdym6z1nIh/x6O+4QJBk/w++Q5e7oapbPo8EjS2X3tq6MUJBbiuKxVhQ5BaRPHUl2j3Ky4gGB4vJfIQDw4RGeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-Rdym6z1nIh/x6O+4QJBk/w++Q5e7oapbPo8EjS2X3tq6MUJBbiuKxVhQ5BaRPHUl2j3Ky4gGB4vJfIQDw4RGeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
|
||||||
<!-- Accent palette -->
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--accent-purple: #a855f7;
|
--accent-purple: #a855f7;
|
||||||
@@ -32,11 +29,11 @@
|
|||||||
<main id="app" class="w-full max-w-sm p-6 space-y-6 text-center"></main>
|
<main id="app" class="w-full max-w-sm p-6 space-y-6 text-center"></main>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
// ===== Imports =====
|
/******************** Imports ********************/
|
||||||
import { nip19 } from "https://cdn.jsdelivr.net/npm/nostr-tools@2.15.0/+esm";
|
import { nip19 } from "https://cdn.jsdelivr.net/npm/nostr-tools@2.15.0/+esm";
|
||||||
import { SimplePool } from "https://cdn.jsdelivr.net/npm/nostr-tools@2.15.0/pool/+esm";
|
import { SimplePool } from "https://cdn.jsdelivr.net/npm/nostr-tools@2.15.0/pool/+esm";
|
||||||
|
|
||||||
// ===== Config =====
|
/******************** Config ********************/
|
||||||
const RELAYS = [
|
const RELAYS = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://nos.lol",
|
"wss://nos.lol",
|
||||||
@@ -44,108 +41,118 @@
|
|||||||
"wss://relay.primal.net",
|
"wss://relay.primal.net",
|
||||||
"wss://relay.nostr.band",
|
"wss://relay.nostr.band",
|
||||||
];
|
];
|
||||||
const TIMEOUT_MS = 6000;
|
const TIMEOUT_MS = 7000;
|
||||||
|
|
||||||
// ===== Logger helpers =====
|
/******************** Logger ********************/
|
||||||
const log = (...a) => console.log("[tipjar]", ...a);
|
const log = (...a) => console.log("[tipjar]", ...a);
|
||||||
const warn = (...a) => console.warn("[tipjar]", ...a);
|
const warn = (...a) => console.warn("[tipjar]", ...a);
|
||||||
const err = (...a) => console.error("[tipjar]", ...a);
|
const err = (...a) => console.error("[tipjar]", ...a);
|
||||||
window.addEventListener("error", e => err("Uncaught:", e.message, e.error||e));
|
window.addEventListener("error", e => err("Uncaught:", e.message, e.error||e));
|
||||||
window.addEventListener("unhandledrejection", e => err("Promise rejection:", e.reason));
|
window.addEventListener("unhandledrejection", e => err("Promise rejection:", e.reason));
|
||||||
|
|
||||||
// ===== Entry =====
|
/******************** Entry ********************/
|
||||||
init();
|
init();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
const { npub, basePath } = extractNpub();
|
const { npub, basePath } = parseURL();
|
||||||
if (!npub) return renderLanding(basePath);
|
if (!npub) return showLanding(basePath);
|
||||||
if (!npub.startsWith("npub")) return renderLanding(basePath, "That doesn’t look like a valid npub.");
|
if (!npub.startsWith("npub")) return showLanding(basePath, "Please enter a valid npub.");
|
||||||
bootTipJar(npub);
|
loadTipJar(npub);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Extract npub & basePath (keep filename when present) --------
|
/******** URL helpers ********/
|
||||||
function extractNpub() {
|
function parseURL() {
|
||||||
const parts = location.pathname.split("/").filter(Boolean);
|
const pathParts = location.pathname.split("/").filter(Boolean);
|
||||||
let npub = null;
|
let npub = null;
|
||||||
if (parts.length >= 2 && parts[0] === "n") npub = parts[1];
|
if (pathParts.length >= 2 && pathParts[0] === "n") npub = pathParts[1];
|
||||||
else npub = new URLSearchParams(location.search).get("n");
|
else npub = new URLSearchParams(location.search).get("n");
|
||||||
const idx = location.pathname.indexOf("/n/");
|
const idx = location.pathname.indexOf("/n/");
|
||||||
const basePath = idx >= 0 ? location.pathname.slice(0, idx) : location.pathname; // keep .html if any
|
const base = idx >= 0 ? location.pathname.slice(0, idx) : location.pathname;
|
||||||
return { npub, basePath };
|
return { npub, basePath: base };
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Landing page --------
|
/******** LANDING PAGE ********/
|
||||||
function renderLanding(basePath, errorMsg = "") {
|
function showLanding(basePath, msg = "") {
|
||||||
log("Landing", { basePath });
|
|
||||||
document.getElementById("app").innerHTML = `
|
document.getElementById("app").innerHTML = `
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<h1 class="text-3xl font-extrabold accent-gradient">Lightning Tip Jar</h1>
|
<h1 class="text-3xl font-extrabold accent-gradient">Lightning Tip Jar</h1>
|
||||||
<p class="text-gray-400">Enter a <span class="font-mono">npub…</span> to generate a shareable tipping link from their Nostr profile.</p>
|
<p class="text-gray-400">Generate a one‑click tipping page from any Nostr <span class="font-mono">npub</span>.</p>
|
||||||
${errorMsg ? `<p class='text-red-500'>${errorMsg}</p>` : ""}
|
${msg ? `<p class="text-red-500">${msg}</p>` : ""}
|
||||||
<form id="lookupForm" class="space-y-4">
|
<form id="lookup" class="space-y-4">
|
||||||
<input id="npubInput" type="text" required placeholder="npub1…" class="w-full px-4 py-2 rounded-lg bg-gray-800 text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
<input id="npubIn" type="text" placeholder="npub1…" required class="w-full px-4 py-2 rounded-lg bg-gray-800 text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600" />
|
||||||
<button class="btn-primary" type="submit">Generate Tip Page</button>
|
<button type="submit" class="btn-primary">Create Tip Page</button>
|
||||||
</form>
|
</form>
|
||||||
<footer class="text-xs text-gray-500 pt-4">Built with nostr-tools and Lightning ❤</footer>
|
<footer class="text-xs text-gray-500 pt-4">Works entirely client‑side • No servers • 100% Lightning ⚡</footer>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
document.getElementById("lookupForm").addEventListener("submit", (e) => {
|
document.getElementById("lookup").onsubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const value = (document.getElementById("npubInput").value || "").trim();
|
const val = document.getElementById("npubIn").value.trim();
|
||||||
if (!value.startsWith("npub")) return renderLanding(basePath, "Please enter a valid npub starting with ‘npub’." );
|
if (!val.startsWith("npub")) return showLanding(basePath, "npub should start with ‘npub’. Try again.");
|
||||||
|
|
||||||
// Determine redirect style: if current path ends with .html use query param, else use /n/<npub>
|
|
||||||
const isFile = /\.html?$/.test(basePath);
|
const isFile = /\.html?$/.test(basePath);
|
||||||
const target = isFile ? `${basePath}?n=${value}` : `${basePath.replace(/\/$/, "")}/n/${value}`;
|
const dest = isFile ? `${basePath}?n=${val}` : `${basePath.replace(/\/$/, "")}/n/${val}`;
|
||||||
log("Redirect →", target);
|
location.href = dest;
|
||||||
location.href = target;
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Tip‑Jar flow --------
|
/******** TIP‑JAR FLOW ********/
|
||||||
async function bootTipJar(npub) {
|
async function loadTipJar(npub) {
|
||||||
const app = document.getElementById("app");
|
const app = document.getElementById("app");
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<img id="avatar" class="w-24 h-24 rounded-full mx-auto hidden" alt="User avatar" />
|
<img id="avatar" class="w-24 h-24 rounded-full mx-auto hidden" alt="Avatar" />
|
||||||
<h1 id="name" class="text-2xl font-extrabold accent-gradient leading-tight mb-2">Loading…</h1>
|
<h1 id="name" class="text-2xl font-extrabold accent-gradient mb-2">Loading…</h1>
|
||||||
<p id="about" class="text-sm text-gray-400"></p>
|
<p id="about" class="text-sm text-gray-400"></p>
|
||||||
<div id="qrcode" class="mx-auto"></div>
|
<div id="qrcode" class="mx-auto"></div>
|
||||||
<p id="address" class="break-all font-mono text-lg"></p>
|
<p id="address" class="break-all font-mono text-lg"></p>
|
||||||
<button id="copyBtn" class="btn-primary">Copy Address</button>`;
|
<button id="copy" class="btn-primary">Copy Address</button>`;
|
||||||
|
|
||||||
let pubHex;
|
// decode npub
|
||||||
try { pubHex = nip19.decode(npub).data; } catch { return fatal("Invalid npub format"); }
|
let hex;
|
||||||
|
try { hex = nip19.decode(npub).data; log("Hex pubkey", hex); }
|
||||||
|
catch { return fail("Bad npub format"); }
|
||||||
|
|
||||||
const fallbackLn = `${npub}@npub.cash`;
|
const fallback = `${npub}@npub.cash`;
|
||||||
let lnAddr = fallbackLn;
|
let meta = null, addr = fallback;
|
||||||
let meta = null;
|
|
||||||
|
|
||||||
const pool = new SimplePool();
|
const pool = new SimplePool();
|
||||||
const sub = pool.sub(RELAYS, [{ kinds:[0], authors:[pubHex], limit:1 }]);
|
const sub = pool.sub(RELAYS, [{ kinds:[0], authors:[hex], limit:1 }]);
|
||||||
let done = false;
|
let finished = false;
|
||||||
const timer = setTimeout(() => { if(!done){warn("Profile timeout"); finish(); }}, TIMEOUT_MS);
|
const timer = setTimeout(() => { warn("Timeout: no profile"); wrapUp(); }, TIMEOUT_MS);
|
||||||
|
|
||||||
sub.on("event", evt => { done=true; clearTimeout(timer); try{ meta=JSON.parse(evt.content||"{}"); lnAddr = meta.lud16||meta.lud06||fallbackLn;}catch(e){err(e);} finish(); });
|
sub.on("event", (ev, relay) => {
|
||||||
sub.on("eose", () => { if(!done){warn("EOSE without meta"); finish(); }});
|
log("Profile", ev.id, "from", relay);
|
||||||
|
clearTimeout(timer); finished = true;
|
||||||
|
try {
|
||||||
|
meta = JSON.parse(ev.content || "{}");
|
||||||
|
addr = meta.lud16 || meta.lud06 || fallback;
|
||||||
|
} catch(e) { err("Meta parse", e); }
|
||||||
|
wrapUp();
|
||||||
|
});
|
||||||
|
sub.on("notice", (n) => warn("Relay notice", n));
|
||||||
|
sub.on("eose", () => { if (!finished) warn("EOSE no profile"); });
|
||||||
|
|
||||||
function finish(){ try{sub.unsub(); pool.close(RELAYS);}catch(_){} renderTip(meta, lnAddr); }
|
function wrapUp() {
|
||||||
function fatal(msg){ app.innerHTML = `<p class='text-red-500'>${msg}</p>`; }
|
try { sub.unsub(); pool.close(RELAYS); } catch {}
|
||||||
|
render(meta, addr);
|
||||||
|
}
|
||||||
|
function fail(msg){ app.innerHTML = `<p class='text-red-500'>${msg}</p>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Render final UI --------
|
/******** RENDER TIP CARD ********/
|
||||||
function renderTip(meta, ln) {
|
function render(meta, ln) {
|
||||||
document.getElementById("name").textContent = meta?.name || "Lightning Tip Jar";
|
document.getElementById("name").textContent = meta?.name || "Lightning Tip Jar";
|
||||||
if (meta?.about) document.getElementById("about").textContent = meta.about;
|
document.getElementById("about").textContent = meta?.about || "";
|
||||||
if (meta?.picture) { const img = document.getElementById("avatar"); img.src = meta.picture; img.classList.remove("hidden"); }
|
if (meta?.picture){ const img = document.getElementById("avatar"); img.src = meta.picture; img.classList.remove("hidden"); }
|
||||||
|
if (!meta?.lud16 && !meta?.lud06) document.getElementById("about").textContent += "\n(No lightning address on profile; using fallback)";
|
||||||
|
|
||||||
document.getElementById("address").textContent = ln;
|
document.getElementById("address").textContent = ln;
|
||||||
const qrWrap = document.getElementById("qrcode"); qrWrap.innerHTML = "";
|
const qr = document.getElementById("qrcode"); qr.innerHTML = "";
|
||||||
new QRCode(qrWrap, { text: ln, width: 200, height: 200, colorDark: "#fff", colorLight: "#000", correctLevel: QRCode.CorrectLevel.H });
|
new QRCode(qr, { text: ln, width: 200, height: 200, colorDark: "#fff", colorLight: "#000", correctLevel: QRCode.CorrectLevel.H });
|
||||||
|
|
||||||
document.getElementById("copyBtn").onclick = () => {
|
document.getElementById("copy").onclick = () => {
|
||||||
navigator.clipboard.writeText(ln).then(() => {
|
navigator.clipboard.writeText(ln).then(() => {
|
||||||
const btn = document.getElementById("copyBtn");
|
const btn = document.getElementById("copy"); const old = btn.textContent;
|
||||||
const old = btn.textContent; btn.textContent = "Copied!"; setTimeout(() => btn.textContent = old, 1500);
|
btn.textContent = "Copied!"; setTimeout(()=>btn.textContent=old, 1500);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user