diff --git a/README.md b/README.md index 1d95bff..9e73b0f 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,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_WEBHOOK_URL` | unset | Webhook endpoint for payment events (requires `GP_WEBHOOK_SECRET`) | | `GP_WEBHOOK_SECRET` | unset | HMAC-SHA256 secret for signing webhooks | -| `GP_QR_LOGO` | GoblinPay mark | Checkout QR center logo: unset = GoblinPay mark, `off`/`none` = plain, else a URL/path | +| `GP_QR_LOGO` | off | Checkout QR center logo: unset/`off`/`none` = plain QR (default), `builtin` = inline Goblin mark, else an image URL | | `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_PAYER_RECEIPT` | `off` | Send a NIP-17 receipt DM to the payer | diff --git a/crates/gp-core/src/config.rs b/crates/gp-core/src/config.rs index bf4fb8d..8538a83 100644 --- a/crates/gp-core/src/config.rs +++ b/crates/gp-core/src/config.rs @@ -192,9 +192,10 @@ pub struct Config { /// HMAC secret for signing webhooks (`GP_WEBHOOK_SECRET`). #[serde(skip)] pub webhook_secret: Option, - /// Center-logo source for checkout QR codes (`GP_QR_LOGO`): unset or - /// `builtin`/`goblin` = the inlined Goblin mark (self-contained, the - /// default), `off`/`none` = no logo, else a URL used as an external image. + /// Center-logo source for checkout QR codes (`GP_QR_LOGO`): unset (or + /// `off`/`none`) = a plain QR, the default; `builtin`/`goblin` = the + /// inlined Goblin mark (self-contained, opt-in); else a URL used as an + /// external image. pub qr_logo: Option, /// Merchant npub for confirmed-payment DMs (`GP_MERCHANT_NPUB`). pub merchant_npub: Option, @@ -235,10 +236,10 @@ pub const DEFAULT_RATE_CACHE_TTL: i64 = 60; /// Default quote lock window (seconds). pub const DEFAULT_QUOTE_TTL: i64 = 900; -/// Sentinel `qr_logo` value selecting the inlined Goblin mark (the default -/// when `GP_QR_LOGO` is unset). Kept out of the `Href` space so it is never -/// treated as a URL. -pub const DEFAULT_QR_LOGO: &str = "builtin"; +/// Sentinel `qr_logo` value selecting the inlined Goblin mark +/// (`GP_QR_LOGO=builtin`, opt-in; the default is a plain QR). Kept out of the +/// `Href` space so it is never treated as a URL. +pub const QR_LOGO_BUILTIN: &str = "builtin"; impl Default for Config { fn default() -> Self { @@ -265,7 +266,7 @@ impl Default for Config { admin_token: None, webhook_url: None, webhook_secret: None, - qr_logo: Some(DEFAULT_QR_LOGO.into()), + qr_logo: None, merchant_npub: None, notify_merchant_dm: false, notify_payer_receipt: false, @@ -375,8 +376,8 @@ impl Config { let webhook_url = get("GP_WEBHOOK_URL").filter(|s| !s.trim().is_empty()); let webhook_secret = secret(get, "GP_WEBHOOK_SECRET")?; let qr_logo = match get("GP_QR_LOGO").as_deref() { - None | Some("builtin") | Some("goblin") => Some(DEFAULT_QR_LOGO.to_string()), - Some("off") | Some("none") | Some("") => None, + None | Some("off") | Some("none") | Some("") => None, + Some("builtin") | Some("goblin") => Some(QR_LOGO_BUILTIN.to_string()), Some(other) => Some(other.to_string()), }; let merchant_npub = get("GP_MERCHANT_NPUB").filter(|s| !s.trim().is_empty()); @@ -455,7 +456,7 @@ impl Config { pub fn qr_logo(&self) -> crate::qr::Logo<'_> { match self.qr_logo.as_deref() { None => crate::qr::Logo::None, - Some(DEFAULT_QR_LOGO) => crate::qr::Logo::Builtin, + Some(QR_LOGO_BUILTIN) => crate::qr::Logo::Builtin, Some(href) => crate::qr::Logo::Href(href), } } @@ -819,7 +820,7 @@ mod tests { fn m5_m6_defaults_and_overrides() { let cfg = load(&[]).unwrap(); assert_eq!(cfg.public_url, format!("http://{DEFAULT_BIND}")); - assert_eq!(cfg.qr_logo.as_deref(), Some(DEFAULT_QR_LOGO)); + assert!(cfg.qr_logo.is_none(), "plain QR by default"); assert!(!cfg.notify_merchant_dm); assert!(!cfg.notify_payer_receipt); assert_eq!(cfg.endpub_rotate_interval, 0); @@ -842,6 +843,16 @@ mod tests { assert!(cfg.notify_merchant_dm); assert_eq!(cfg.endpub_rotate_interval, 3600); assert_eq!(cfg.endpub_overlap_epochs, 2); + + // The center logo is opt-in: builtin sentinel or a custom image URL. + let cfg = load(&[("GP_QR_LOGO", "builtin")]).unwrap(); + assert_eq!(cfg.qr_logo.as_deref(), Some(QR_LOGO_BUILTIN)); + assert_eq!(cfg.qr_logo(), crate::qr::Logo::Builtin); + let cfg = load(&[("GP_QR_LOGO", "https://cdn.example/logo.svg")]).unwrap(); + assert_eq!( + cfg.qr_logo(), + crate::qr::Logo::Href("https://cdn.example/logo.svg") + ); } #[test] diff --git a/crates/gp-core/src/qr.rs b/crates/gp-core/src/qr.rs index ea92e8b..539c292 100644 --- a/crates/gp-core/src/qr.rs +++ b/crates/gp-core/src/qr.rs @@ -2,13 +2,12 @@ //! //! The QR is always generated at error-correction level **H** (tolerates //! ~30% occlusion) so an optional centered logo, sized to ~22% of the code, -//! never breaks scannability. The default logo is the Goblin mark, drawn as -//! inline vector shapes (not a referenced ``) so the whole QR is -//! self-contained: it renders identically wherever the SVG is embedded, -//! including cross-origin connector pages (WooCommerce, Medusa) where a -//! `/static/...` href would 404. An operator may instead point `GP_QR_LOGO` -//! at their own image URL, or disable the logo entirely. With no logo it is a -//! plain black-on-white QR. +//! never breaks scannability. The default is a plain black-on-white QR. An +//! operator may opt in to a center logo (`GP_QR_LOGO`): the built-in Goblin +//! mark is drawn as inline vector shapes (not a referenced ``) so the +//! whole QR is self-contained and renders identically wherever the SVG is +//! embedded, including cross-origin connector pages (WooCommerce, Medusa) +//! where a `/static/...` href would 404; a custom image URL is also accepted. //! //! Rendering is hand-rolled (one `` of the dark modules) so the crate //! needs only the `qrcode` matrix, not its image/SVG feature or any raster @@ -27,7 +26,7 @@ pub enum Logo<'a> { /// No center logo: a plain black-on-white QR. None, /// The built-in Goblin mark, inlined as vector shapes. Self-contained, so - /// it renders on any origin (this is the default). + /// it renders on any origin (opt-in via `GP_QR_LOGO=builtin`). Builtin, /// An operator-supplied image referenced by URL (`GP_QR_LOGO=`). The /// operator is responsible for it being reachable from wherever the QR is