checkout: show the GoblinPay mark on the QR and the pay page

The hosted checkout page gains the GoblinPay wordmark header, and the QR
center logo defaults to a new high-contrast GoblinPay mark (dark P on
brand gold) instead of the generic goblin mark. Still overridable via
GP_QR_LOGO (url/path/off); the legacy /static/goblin-mark.svg route is
kept for operators who pinned it.
This commit is contained in:
2ro
2026-07-02 17:00:36 -04:00
parent c362a9af21
commit 3a80f7d505
7 changed files with 86 additions and 6 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ carries the full merchant surface:
invoice automatically. invoice automatically.
- **Hosted checkout:** a zero-JS `/pay/<token>` page (server-rendered - **Hosted checkout:** a zero-JS `/pay/<token>` page (server-rendered
Askama + one CSS file + a server-generated QR SVG at ECC level H with an Askama + one CSS file + a server-generated QR SVG at ECC level H with an
optional Goblin-mark center logo), live status via `<meta http-equiv=refresh>`, optional GoblinPay-mark center logo), live status via `<meta http-equiv=refresh>`,
and a manual slatepack fallback (paste S1 -> offline `receive_tx` -> copy the and a manual slatepack fallback (paste S1 -> offline `receive_tx` -> copy the
S2 back) on every page. The same renderer serves embedded and hosted use. S2 back) on every page. The same renderer serves embedded and hosted use.
- **Per-user endpubs:** an admin assigns one receiving identity per user - **Per-user endpubs:** an admin assigns one receiving identity per user
@@ -75,7 +75,7 @@ Everything is environment variables, defaults are safe for local use.
| `GP_ADMIN_TOKEN` | unset | Bearer token for the admin dashboard + endpub/webhook API | | `GP_ADMIN_TOKEN` | unset | Bearer token for the admin dashboard + endpub/webhook API |
| `GP_WEBHOOK_URL` | unset | Webhook endpoint for payment events (requires `GP_WEBHOOK_SECRET`) | | `GP_WEBHOOK_URL` | unset | Webhook endpoint for payment events (requires `GP_WEBHOOK_SECRET`) |
| `GP_WEBHOOK_SECRET` | unset | HMAC-SHA256 secret for signing webhooks | | `GP_WEBHOOK_SECRET` | unset | HMAC-SHA256 secret for signing webhooks |
| `GP_QR_LOGO` | Goblin mark | Checkout QR center logo: unset = Goblin mark, `off`/`none` = plain, else a URL/path | | `GP_QR_LOGO` | GoblinPay mark | Checkout QR center logo: unset = GoblinPay mark, `off`/`none` = plain, else a URL/path |
| `GP_MERCHANT_NPUB` | unset | Merchant npub for the NIP-17 confirmed-payment DM | | `GP_MERCHANT_NPUB` | unset | Merchant npub for the NIP-17 confirmed-payment DM |
| `GP_NOTIFY_MERCHANT_DM` | `off` | Send a NIP-17 DM to the merchant on a received payment | | `GP_NOTIFY_MERCHANT_DM` | `off` | Send a NIP-17 DM to the merchant on a received payment |
| `GP_NOTIFY_PAYER_RECEIPT` | `off` | Send a NIP-17 receipt DM to the payer | | `GP_NOTIFY_PAYER_RECEIPT` | `off` | Send a NIP-17 receipt DM to the payer |
+2 -2
View File
@@ -171,7 +171,7 @@ pub struct Config {
#[serde(skip)] #[serde(skip)]
pub webhook_secret: Option<Secret>, pub webhook_secret: Option<Secret>,
/// Center-logo source for checkout QR codes (`GP_QR_LOGO`): unset = the /// Center-logo source for checkout QR codes (`GP_QR_LOGO`): unset = the
/// bundled Goblin mark, `off`/`none` = no logo, else a URL or static path. /// bundled GoblinPay mark, `off`/`none` = no logo, else a URL or static path.
pub qr_logo: Option<String>, pub qr_logo: Option<String>,
/// Merchant npub for confirmed-payment DMs (`GP_MERCHANT_NPUB`). /// Merchant npub for confirmed-payment DMs (`GP_MERCHANT_NPUB`).
pub merchant_npub: Option<String>, pub merchant_npub: Option<String>,
@@ -213,7 +213,7 @@ pub const DEFAULT_RATE_CACHE_TTL: i64 = 60;
pub const DEFAULT_QUOTE_TTL: i64 = 900; pub const DEFAULT_QUOTE_TTL: i64 = 900;
/// Default center-logo path served by gp-server when `GP_QR_LOGO` is unset. /// Default center-logo path served by gp-server when `GP_QR_LOGO` is unset.
pub const DEFAULT_QR_LOGO: &str = "/static/goblin-mark.svg"; pub const DEFAULT_QR_LOGO: &str = "/static/goblinpay-mark.svg";
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
+23 -2
View File
@@ -44,19 +44,40 @@ async fn style() -> impl Responder {
.body(include_str!("../../../static/style.css")) .body(include_str!("../../../static/style.css"))
} }
/// The bundled Goblin mark, the default QR center logo. /// The bundled Goblin mark (legacy default QR center logo; still served so an
/// operator can keep `GP_QR_LOGO=/static/goblin-mark.svg`).
async fn goblin_mark() -> impl Responder { async fn goblin_mark() -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
.content_type("image/svg+xml") .content_type("image/svg+xml")
.body(include_str!("../../../static/goblin-mark.svg")) .body(include_str!("../../../static/goblin-mark.svg"))
} }
/// The GoblinPay mark, the default QR center logo (dark "P" on the brand gold,
/// sized for contrast on the QR's white backing).
async fn goblinpay_mark() -> impl Responder {
HttpResponse::Ok()
.content_type("image/svg+xml")
.body(include_str!("../../../static/goblinpay-mark.svg"))
}
/// The GoblinPay wordmark (white), shown as the checkout page header logo.
async fn goblinpay_wordmark() -> impl Responder {
HttpResponse::Ok()
.content_type("image/svg+xml")
.body(include_str!("../../../static/goblinpay-wordmark.svg"))
}
/// Route table, shared by `main` and the tests. /// Route table, shared by `main` and the tests.
fn routes(cfg: &mut web::ServiceConfig) { fn routes(cfg: &mut web::ServiceConfig) {
cfg.route("/", web::get().to(index)) cfg.route("/", web::get().to(index))
.route("/health", web::get().to(health)) .route("/health", web::get().to(health))
.route("/static/style.css", web::get().to(style)) .route("/static/style.css", web::get().to(style))
.route("/static/goblin-mark.svg", web::get().to(goblin_mark)); .route("/static/goblin-mark.svg", web::get().to(goblin_mark))
.route("/static/goblinpay-mark.svg", web::get().to(goblinpay_mark))
.route(
"/static/goblinpay-wordmark.svg",
web::get().to(goblinpay_wordmark),
);
// Payment status + signed-receipt reads (public-by-token, M4). // Payment status + signed-receipt reads (public-by-token, M4).
payments::configure(cfg); payments::configure(cfg);
// Hosted checkout + manual slatepack (public-by-token, M5). // Hosted checkout + manual slatepack (public-by-token, M5).
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="GoblinPay">
<rect width="64" height="64" rx="14" fill="#e9c542"/>
<path fill="#201d09" fill-rule="evenodd" d="M22 14H35a12 12 0 0 1 0 24H30V50H22ZM30 21H34a6 6 0 0 1 0 12H30Z"/>
</svg>

After

Width:  |  Height:  |  Size: 272 B

+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="600.000000pt" height="232.000000pt" viewBox="0 0 600.000000 232.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,232.000000) scale(0.050000,-0.050000)"
fill="#ffffff" stroke="none">
<path d="M52 4522 c-131 -444 37 -874 416 -1064 l122 -61 -95 11 c-111 13
-226 67 -324 154 -83 72 -86 66 -28 -48 140 -278 398 -434 715 -434 197 0 331
45 362 120 62 150 316 340 520 391 271 67 273 570 2 813 -194 175 -626 253
-842 152 l-70 -33 91 -1 c191 -4 428 -112 506 -231 36 -53 15 -72 -26 -23 -89
108 -361 154 -623 104 -384 -72 -552 -22 -664 197 l-30 60 -32 -107z"/>
<path d="M4560 2533 l0 -1610 285 3 285 4 5 573 6 573 484 9 c534 10 636 29
838 158 583 370 592 1348 15 1718 -240 155 -320 167 -1163 175 l-755 7 0
-1610z m1450 1080 c389 -170 416 -801 42 -998 -75 -40 -118 -44 -497 -51
l-415 -8 0 554 0 554 395 -8 c319 -7 410 -15 475 -43z"/>
<path d="M3139 3938 c-92 -222 -202 -268 -639 -268 -462 0 -625 -70 -783 -338
-58 -100 -60 -71 -4 66 37 92 27 95 -96 29 -298 -160 -537 -548 -537 -871 0
-117 -1 -117 -150 -4 -124 94 -359 217 -456 240 l-55 13 43 -108 c23 -59 67
-230 98 -381 87 -422 215 -636 412 -686 l94 -23 -4 -152 c-8 -255 71 -431 252
-564 108 -79 133 -87 119 -36 -6 19 -16 75 -24 125 l-14 90 86 -87 c155 -155
391 -243 648 -243 238 1 257 17 94 80 -148 58 -352 193 -453 300 l-60 63 150
-81 c278 -149 618 -181 915 -86 79 25 130 33 138 20 43 -71 324 -122 433 -79
31 12 26 23 -37 83 -138 133 -143 166 -41 260 116 107 160 201 145 310 -11 78
-6 89 69 162 122 118 151 178 228 468 39 147 96 319 127 383 65 137 62 139
-143 85 -158 -41 -258 -93 -362 -188 l-84 -77 -33 95 c-42 118 -118 202 -181
202 -63 0 -316 127 -366 183 -21 23 -29 39 -18 34 11 -5 54 -25 96 -44 600
-273 1151 104 1154 791 l0 94 -101 -107 c-197 -209 -486 -299 -738 -231 l-88
23 64 51 c95 74 147 190 147 330 0 142 -10 158 -45 74z m-172 -1574 c115 -128
161 -505 71 -595 -162 -162 -289 -89 -286 164 3 327 112 544 215 431z m-1030
-33 c101 -52 201 -243 241 -459 71 -385 -574 -317 -668 70 -55 226 234 489
427 389z m1703 -77 c0 -181 -102 -341 -141 -222 l-24 74 -27 -63 c-47 -112
-128 -19 -128 146 0 76 37 97 54 31 12 -47 46 -54 46 -9 0 48 62 61 92 19 26
-35 27 -35 41 2 37 103 87 116 87 22z m-2770 -34 c16 -73 41 -77 58 -10 33
129 140 -19 128 -177 l-6 -83 -43 66 -42 66 -13 -51 c-14 -56 -66 -69 -85 -21
-17 45 -36 36 -61 -30 -50 -132 -103 -48 -68 108 39 175 108 243 132 132z
m1350 -837 c0 -123 205 -183 399 -116 109 38 122 31 46 -24 -212 -151 -633
-30 -516 148 43 65 71 62 71 -8z"/>
<path d="M7910 3299 c-533 -109 -818 -523 -817 -1189 0 -436 84 -696 300 -929
341 -369 1014 -413 1335 -88 l90 92 -13 -78 c-7 -42 -13 -99 -14 -127 l-1 -50
250 -6 c138 -3 270 -1 295 6 l45 11 0 1159 0 1160 -281 0 -281 0 14 -125 14
-125 -82 84 c-183 188 -531 271 -854 205z m590 -485 c333 -171 432 -878 177
-1262 -222 -336 -773 -264 -938 121 -297 691 196 1430 761 1141z"/>
<path d="M9680 3249 c0 -6 211 -496 469 -1090 l469 -1078 -84 -201 c-162 -386
-315 -476 -665 -388 l-49 12 0 -231 c0 -269 -11 -255 217 -268 402 -22 736
151 909 470 43 81 553 1429 994 2630 l57 155 -296 0 -295 0 -256 -730 c-141
-402 -260 -726 -265 -722 -5 5 -146 331 -314 725 l-305 717 -293 6 c-161 3
-293 0 -293 -7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+3
View File
@@ -31,6 +31,9 @@ main {
main.admin { max-width: 60rem; } main.admin { max-width: 60rem; }
.brand { display: block; }
.brandmark { height: 30px; width: auto; display: block; margin: 0 0 1.25rem; }
h1 { font-size: 1.5rem; margin: 0 0 0.5rem; } h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
h2 { font-size: 1.05rem; margin: 1.75rem 0 0.5rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.04em; } h2 { font-size: 1.05rem; margin: 1.75rem 0 0.5rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.04em; }
+1
View File
@@ -9,6 +9,7 @@
</head> </head>
<body> <body>
<main class="checkout"> <main class="checkout">
<a class="brand" href="/"><img class="brandmark" src="/static/goblinpay-wordmark.svg" alt="GoblinPay"></a>
<h1>Pay with Goblin</h1> <h1>Pay with Goblin</h1>
<p class="amount">{{ info.amount_display }}</p> <p class="amount">{{ info.amount_display }}</p>