bank landing: one-step withdraw-exchange + Android/iOS links

Short intro; wallet install; single taler://withdraw-exchange QR.
This commit is contained in:
Hernâni Marques 2026-07-10 18:05:00 +02:00
parent 8729e69e87
commit 5af039ca03
No known key found for this signature in database
2 changed files with 96 additions and 145 deletions

View file

@ -13,33 +13,25 @@ open your own bank account, and point merchants at `taler.hacktivism.ch`.
## User flow (documented on the page) ## User flow (documented on the page)
### Two steps (only path) Same **one-step** pattern as [taler-ops.ch Android/iOS guides](https://taler-ops.ch/en/users-android.html):
1. **`taler://add-exchange/exchange.hacktivism.ch/`** — add exchange, accept ToS. 1. **Install wallet** — Android (Play Store / F-Droid) and iOS (App Store), plus hub `wallet.taler.net`.
2. **Demo withdraw URI** from `withdraw.uri` — bank-integrated withdraw (default `GOA:10`). 2. **One QR / link**`taler://withdraw-exchange/exchange.hacktivism.ch/`
Adds the GOA exchange **and** starts withdrawal (accept ToS → enter amount).
If the demo withdraw is already **confirmed/aborted**, the page points at the bank UI instead. The old two-step path (`taler://add-exchange/…` + demo `taler://withdraw/…` from `withdraw.uri`) is **removed** from the landing.
Optional: bank UI (`/webui/`) and merchant (`taler.hacktivism.ch`) stay further down.
### CLI sketch (fresh wallet DB) ### CLI sketch (fresh wallet DB)
```bash ```bash
# equivalent one-step in CLI terms:
taler-wallet-cli exchanges add https://exchange.hacktivism.ch/ taler-wallet-cli exchanges add https://exchange.hacktivism.ch/
taler-wallet-cli exchanges accept-tos https://exchange.hacktivism.ch/ taler-wallet-cli exchanges accept-tos https://exchange.hacktivism.ch/
taler-wallet-cli withdraw accept-uri --exchange https://exchange.hacktivism.ch/ 'taler://withdraw/…' # then withdraw from the exchange (amount chosen in wallet UI)
# then bank confirm (demo often auto-confirms) + run-until-done
``` ```
### URI note (`:443`)
libeufin may emit:
```text
taler://withdraw/bank.hacktivism.ch:443/taler-integration/<uuid>
```
Explicit default HTTPS port. Valid for wallets; the landing page **strips `:443`** for
QR / open links when the host is the public bank name. Both forms work.
--- ---
## Live stats (in bank container) ## Live stats (in bank container)

View file

@ -66,7 +66,7 @@
.sub { .sub {
color: var(--muted); color: var(--muted);
margin: 0 auto; margin: 0 auto;
max-width: 28rem; max-width: 30rem;
font-size: 1.05rem; font-size: 1.05rem;
line-height: 1.45; line-height: 1.45;
} }
@ -83,7 +83,6 @@
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
/* Big step cards */
.step-card { .step-card {
background: var(--card); background: var(--card);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
@ -168,7 +167,9 @@
font-size: 0.85rem; font-size: 0.85rem;
color: #3a342c; color: #3a342c;
font-weight: 700; font-weight: 700;
max-width: 220px; max-width: 240px;
margin-left: auto;
margin-right: auto;
} }
.cta { .cta {
display: inline-block; display: inline-block;
@ -196,15 +197,6 @@
text-align: center; text-align: center;
max-width: 100%; max-width: 100%;
} }
#status {
min-height: 1.1em;
font-size: 0.85rem;
color: var(--muted);
margin: 0.4rem 0 0;
text-align: center;
}
#status.err { color: #f0a090; }
#status.ok { color: var(--accent2); }
code { code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.86em; font-size: 0.86em;
@ -214,7 +206,42 @@
color: #f0d090; color: #f0d090;
} }
/* Distinct “more options” section further down */ /* App store buttons */
.app-links {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
justify-content: center;
width: 100%;
}
.app-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-width: 9.5rem;
text-decoration: none;
font-weight: 700;
font-size: 0.92rem;
padding: 0.65rem 0.95rem;
border-radius: 11px;
border: 1px solid rgba(232, 168, 56, 0.4);
background: rgba(0, 0, 0, 0.28);
color: #f0d090;
}
.app-btn:hover { filter: brightness(1.08); border-color: rgba(240, 200, 106, 0.7); }
.app-btn.ios {
border-color: rgba(62, 207, 191, 0.4);
color: #b8f5ec;
}
.app-btn.hub {
flex: 1 1 100%;
border-color: rgba(201, 184, 160, 0.35);
color: var(--muted);
font-weight: 600;
font-size: 0.85rem;
}
.more-section { .more-section {
margin-top: 2.75rem; margin-top: 2.75rem;
padding-top: 1.75rem; padding-top: 1.75rem;
@ -285,7 +312,6 @@
transform-origin: center top; transform-origin: center top;
opacity: 0.95; opacity: 0.95;
} }
/* Live stats (written by in-container landing-stats.sh → stats.json) */
.stats { .stats {
background: rgba(18, 28, 28, 0.55); background: rgba(18, 28, 28, 0.55);
border: 1px solid rgba(62, 207, 191, 0.28); border: 1px solid rgba(62, 207, 191, 0.28);
@ -335,7 +361,6 @@
text-align: center; text-align: center;
} }
.stat .v.teal { color: #5eead4; } .stat .v.teal { color: #5eead4; }
.stat .v.muted { color: var(--muted); font-size: 0.92rem; font-weight: 600; }
.stat.wide { grid-column: 1 / -1; } .stat.wide { grid-column: 1 / -1; }
.wd-list { .wd-list {
list-style: none; list-style: none;
@ -391,14 +416,14 @@
<body> <body>
<main> <main>
<header class="hero"> <header class="hero">
<span class="badge">Intergalactic · GOA · no IBAN</span> <span class="badge">Exploration · GOA</span>
<h1>GOA Exploration Bank</h1> <h1>GOA Exploration Bank</h1>
<p class="sub"> <p class="sub">
Two steps to put <strong>GOA</strong> in your GNU Taler wallet. Install the wallet, then <strong>one QR</strong> adds the exchange and starts a withdraw.
</p> </p>
</header> </header>
<!-- ========== LIVE STATS (stats.json from bank container) ========== --> <!-- ========== LIVE STATS ========== -->
<section class="stats" id="stats" aria-labelledby="stats-title" hidden> <section class="stats" id="stats" aria-labelledby="stats-title" hidden>
<h2 id="stats-title">GOA flow</h2> <h2 id="stats-title">GOA flow</h2>
<div class="stats-grid"> <div class="stats-grid">
@ -483,64 +508,62 @@
<p class="stats-foot" id="st-perf-foot">Memory from /proc + cgroup inside podman container.</p> <p class="stats-foot" id="st-perf-foot">Memory from /proc + cgroup inside podman container.</p>
</section> </section>
<!-- ========== STEP 1 ========== --> <!-- ========== STEP 1: install apps ========== -->
<section class="step-card" aria-labelledby="step1-title"> <section class="step-card" aria-labelledby="step1-title">
<h2 id="step1-title"><span class="step-num">1</span> Add the exchange</h2> <h2 id="step1-title"><span class="step-num">1</span> Get the wallet</h2>
<p class="step-tagline">Tell the wallet where GOA coins come from.</p> <p class="step-tagline">Android or iOS</p>
<div class="step-body"> <div class="step-body">
<ol> <div class="app-links">
<li>Install the <strong>GNU Taler Wallet</strong> (phone app or browser extension).</li> <a class="app-btn"
<li>Scan the QR with the wallet, or tap the button.</li> href="https://play.google.com/store/apps/details?id=net.taler.wallet"
<li>Confirm / trust <code>exchange.hacktivism.ch</code>.</li> target="_blank" rel="noopener noreferrer">
</ol> Android · Play Store
<div class="qr-box"> </a>
<div id="qr-exchange"></div> <a class="app-btn"
<div class="qr-label">Add exchange in wallet</div> href="https://f-droid.org/en/packages/net.taler.wallet.fdroid/"
target="_blank" rel="noopener noreferrer">
Android · F-Droid
</a>
<a class="app-btn ios"
href="https://apps.apple.com/app/taler-wallet/id6463440117"
target="_blank" rel="noopener noreferrer">
iOS · App Store
</a>
<a class="app-btn hub"
href="https://wallet.taler.net/"
target="_blank" rel="noopener noreferrer">
All platforms · wallet.taler.net
</a>
</div> </div>
<a class="cta" id="open-exchange" href="taler://add-exchange/exchange.hacktivism.ch/">
Open in wallet app →
</a>
<p class="meta">taler://add-exchange/exchange.hacktivism.ch/</p>
</div> </div>
</section> </section>
<!-- ========== STEP 2 ========== --> <!-- ========== STEP 2: one-step withdraw-exchange ========== -->
<section class="step-card step-money" aria-labelledby="step2-title"> <section class="step-card step-money" aria-labelledby="step2-title">
<h2 id="step2-title"><span class="step-num">2</span> Get the money</h2> <h2 id="step2-title"><span class="step-num">2</span> Withdraw GOA</h2>
<p class="step-tagline">Withdraw GOA into the wallet you just set up.</p> <p class="step-tagline">One-step: add exchange + withdraw</p>
<div class="step-body"> <div class="step-body">
<ol>
<li>Scan this second QR with the <strong>same wallet</strong>.</li>
<li>Default amount is <code>GOA:10</code> — confirm in the wallet.</li>
<li>If the wallet waits on a bank transfer: leave it open a moment — demo withdrawals are auto-confirmed.</li>
</ol>
<div class="qr-box"> <div class="qr-box">
<div id="qr-withdraw"></div> <div id="qr-withdraw-exchange"></div>
<div class="qr-label" id="qr-withdraw-label">Withdraw GOA:10</div> <div class="qr-label">Scan or open in wallet</div>
</div> </div>
<a class="cta teal" id="open-wallet" href="#">Open withdraw in wallet →</a> <a class="cta teal" id="open-withdraw-exchange"
<p id="status">Preparing withdraw QR…</p> href="taler://withdraw-exchange/exchange.hacktivism.ch/">
<p class="meta" id="uri-line" hidden></p> Open in wallet →
</a>
<p class="meta">taler://withdraw-exchange/exchange.hacktivism.ch/</p>
</div> </div>
</section> </section>
<!-- ========== FURTHER DOWN: own account ========== --> <!-- ========== Optional bank UI ========== -->
<div class="more-section"> <div class="more-section">
<span class="more-label">Optional · own bank account</span> <span class="more-label">Optional · bank UI &amp; merchant</span>
<div class="more-card"> <div class="more-card">
<h2>Need your own balance or merchant setup?</h2> <h2>Own bank account or shop?</h2>
<p> <p>
The two steps above use a <strong>demo withdraw</strong> prepared by the bank. Register in the bank UI for your own balance, or set up a shop at
For your own GOA balance, repeated withdraws, or receiving payments as a merchant, <a href="https://taler.hacktivism.ch/" style="color:#f0c86a">taler.hacktivism.ch</a>.
create an account:
</p> </p>
<ol>
<li>Register in the bank UI (username + password).</li>
<li>Get a credit (transfer / ops) — new accounts start at <code>GOA:0</code>.</li>
<li>Log in → <strong>Withdraw</strong> (wallet still needs the exchange from step 1).</li>
<li>To accept payments: set up a shop at
<a href="https://taler.hacktivism.ch/" style="color:#f0c86a">taler.hacktivism.ch</a>.</li>
</ol>
<div class="more-qr"> <div class="more-qr">
<div class="qr-box"> <div class="qr-box">
<div id="qr-webui"></div> <div id="qr-webui"></div>
@ -548,7 +571,7 @@
</div> </div>
</div> </div>
<div class="more-actions"> <div class="more-actions">
<a class="cta-sec" href="/webui/">Open bank UI (register / login)</a> <a class="cta-sec" href="/webui/">Open bank UI →</a>
<a class="cta-sec merchant" href="https://taler.hacktivism.ch/">Merchant backend →</a> <a class="cta-sec merchant" href="https://taler.hacktivism.ch/">Merchant backend →</a>
</div> </div>
</div> </div>
@ -572,17 +595,8 @@
<script> <script>
(function () { (function () {
var WEBUI = "https://bank.hacktivism.ch/webui/"; var WEBUI = "https://bank.hacktivism.ch/webui/";
var ADD_EXCHANGE = "taler://add-exchange/exchange.hacktivism.ch/"; /* taler-ops one-step: add exchange + start withdraw */
var statusEl = document.getElementById("status"); var WITHDRAW_EXCHANGE = "taler://withdraw-exchange/exchange.hacktivism.ch/";
var uriLine = document.getElementById("uri-line");
var openWallet = document.getElementById("open-wallet");
var label = document.getElementById("qr-withdraw-label");
function setStatus(msg, cls) {
if (!statusEl) return;
statusEl.textContent = msg;
statusEl.className = cls || "";
}
function makeQr(el, text) { function makeQr(el, text) {
if (!el) return; if (!el) return;
@ -599,35 +613,12 @@
}); });
} }
makeQr(document.getElementById("qr-exchange"), ADD_EXCHANGE); makeQr(document.getElementById("qr-withdraw-exchange"), WITHDRAW_EXCHANGE);
makeQr(document.getElementById("qr-webui"), WEBUI); makeQr(document.getElementById("qr-webui"), WEBUI);
var defaultAmount = "GOA:10";
function strip443(uri) {
if (!uri) return uri;
return uri.replace(/bank\.hacktivism\.ch:443/g, "bank.hacktivism.ch");
}
function useUri(uri, amount) {
if (!uri) return false;
uri = strip443(uri.trim());
var amt = amount || defaultAmount;
makeQr(document.getElementById("qr-withdraw"), uri);
if (openWallet) openWallet.href = uri;
if (uriLine) {
uriLine.hidden = false;
uriLine.textContent = uri;
}
if (label) label.textContent = "Withdraw " + amt;
setStatus("Ready — default " + amt + ". Scan with the wallet (after step 1).", "ok");
return true;
}
function fmtCest(unix, fallback) { function fmtCest(unix, fallback) {
var base = ""; var base = "";
if (fallback) { if (fallback) {
// strip trailing CEST/CET/Z if present; we append Europe/Zurich
base = String(fallback).replace(/\s*(CEST|CET|UTC|Z)\s*$/i, "").trim(); base = String(fallback).replace(/\s*(CEST|CET|UTC|Z)\s*$/i, "").trim();
} else if (unix != null && unix !== "") { } else if (unix != null && unix !== "") {
try { try {
@ -769,7 +760,7 @@
note.textContent = ""; note.textContent = "";
} }
}) })
.catch(function (e) { .catch(function () {
if (foot) { if (foot) {
foot.hidden = false; foot.hidden = false;
foot.className = "stats-foot err"; foot.className = "stats-foot err";
@ -778,38 +769,6 @@
}); });
} }
loadStats(); loadStats();
Promise.all([
fetch("withdraw.uri", { cache: "no-store" }).then(function (r) {
return r.ok ? r.text() : Promise.reject();
}),
fetch("withdraw.amount", { cache: "no-store" }).then(function (r) {
return r.ok ? r.text() : Promise.resolve(defaultAmount);
}).catch(function () { return defaultAmount; }),
fetch("stats.json", { cache: "no-store" }).then(function (r) {
return r.ok ? r.json() : null;
}).catch(function () { return null; })
])
.then(function (parts) {
var uri = (parts[0] || "").trim();
var amt = (parts[1] || defaultAmount).trim() || defaultAmount;
var st = parts[2];
if (st && st.demo && st.demo.ready === false) {
// Spent demo withdraw — prefer bank UI path
fallback("Demo withdraw already used (" + (st.demo.status || "?") + ") — open bank UI below or wait for ops refresh.");
return;
}
if (useUri(uri, amt)) return;
fallback();
})
.catch(function () { fallback(); });
function fallback(msg) {
makeQr(document.getElementById("qr-withdraw"), WEBUI);
if (openWallet) openWallet.href = WEBUI;
if (label) label.textContent = "Bank UI (no demo withdraw)";
setStatus(msg || "No demo withdraw right now — use optional bank account below.", "err");
}
})(); })();
</script> </script>
</body> </body>