v0.2.0:41 — Patron implies Pro; BTCPay Connect back to one-click authorize

Patron entitlement now expands to the full Pro surface
(unlimited_products / _policies / _codes, recurring_billing,
zaprite_payments) in tier::current(). Existing Patron customers get
the implied entitlements without re-issuing.

BTCPay Connect: replace the four-field paste form (Base URL + API key
+ Store id + Webhook secret) with the original one-click button that
fetches an authorize URL from /v1/admin/btcpay/connect, opens it in a
new tab, and polls /v1/admin/btcpay/status until the BTCPay callback
finishes. Zaprite path unchanged.
This commit is contained in:
Grant
2026-05-12 12:12:54 -05:00
parent d927e4940f
commit a3662de6d8
3 changed files with 140 additions and 3 deletions
+25 -1
View File
@@ -90,12 +90,36 @@ impl TierInfo {
/// to be fixed.
pub async fn current(state: &AppState) -> TierInfo {
let tier = state.self_tier.read().await;
let entitlements = match &*tier {
let mut entitlements = match &*tier {
Tier::Licensed { entitlements, .. } => entitlements.clone(),
Tier::Unlicensed { .. } => Vec::new(),
};
drop(tier);
// Patron implies Pro by design (see module docstring: "Patron: same
// feature surface as Pro, plus a `patron` entitlement..."). Without
// this expansion, every downstream `tier.has(<pro-entitlement>)`
// check requires the Patron POLICY on the master Keysat to
// redundantly list every Pro entitlement. That's brittle: a single
// missing slug on the policy (e.g. operator forgets
// `zaprite_payments`) breaks Pro-equivalence for every Patron
// customer. Treating `patron` as a strict superset of Pro at the
// resolution layer means policy authors can list `patron` alone
// and have everything Pro grants flow through automatically.
if entitlements.iter().any(|e| e == "patron") {
for implied in [
"unlimited_products",
"unlimited_policies",
"unlimited_codes",
"recurring_billing",
"zaprite_payments",
] {
if !entitlements.iter().any(|e| e == implied) {
entitlements.push(implied.to_string());
}
}
}
let label: &'static str;
let display_name: &'static str;
if entitlements.iter().any(|e| e == "patron") {