qr: plain QR by default (center logo now opt-in)
Owner call: no logo inside payment QR codes. GP_QR_LOGO unset now maps to Logo::None; the inlined Goblin mark stays available as GP_QR_LOGO=builtin and custom image URLs still work. ECC stays at level H (the pre-existing level). Wordmark header and connector page refresh are unchanged.
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -192,9 +192,10 @@ pub struct Config {
|
||||
/// HMAC secret for signing webhooks (`GP_WEBHOOK_SECRET`).
|
||||
#[serde(skip)]
|
||||
pub webhook_secret: Option<Secret>,
|
||||
/// 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<String>,
|
||||
/// Merchant npub for confirmed-payment DMs (`GP_MERCHANT_NPUB`).
|
||||
pub merchant_npub: Option<String>,
|
||||
@@ -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]
|
||||
|
||||
@@ -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 `<image>`) 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 `<image>`) 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 `<path>` 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=<url>`). The
|
||||
/// operator is responsible for it being reachable from wherever the QR is
|
||||
|
||||
Reference in New Issue
Block a user