Bodholdt Licensing
Self-hosted software licensing for WordPress
Get Bodholdt Licensing
Freemius takes 7–15%. We take from $49/year.
Hobby
$49.00
/yr
Self-hosted plugin licensing.
- 1 product, 1 site
- License keys + auto-updater
- Stripe-powered checkout
- Customer portal + Stripe billing portal
- SDK generator for your customers
- Email support while active
Studio
$99
/yr
Everything in Hobby, plus:
- Unlimited products (Hobby covers 1)
- Up to 5 sites
- Reports & analytics — revenue, MRR, license health, seat utilization
Save vs Freemius
Foundry
$199
/yr
Everything in Studio, plus:
- Unlimited sites
- Priority email support while active
- Saves $3,500–$7,500/yr vs Freemius (at $50K/yr in plugin sales)
See full feature comparison →
| Feature | Hobby | Studio | Foundry |
|---|---|---|---|
| Sites | 1 | 5 | ∞ |
| Products you can sell | 1 | ∞ | ∞ |
| License keys + auto-updater | ✓ | ✓ | ✓ |
| Stripe checkout + customer portal | ✓ | ✓ | ✓ |
| SDK generator | ✓ | ✓ | ✓ |
| Reports & analytics | — | ✓ | ✓ |
| Lifetime license option | ✓ | ✓ | ✓ |
| Support | Email while active | Email while active | Priority email |
See It In Action
Real screens from Bodholdt Licensing
These are real admin screens, not mockups. Click any one to view it full size.
A complete self-hosted software licensing system for WordPress. Manage license keys, handle activations, process Stripe payments, and deliver updates, all from your own server.
License key generation and management
Stripe checkout for subscriptions, one-time payments, and trials
SDK generator that produces a drop-in client with an auto-updater and feature gating
Reports and analytics for MRR/ARR, license health, and seat utilization
Automatic, license-authenticated updates
Customer portal plus the Stripe billing portal
Product bundles and per-tier feature gating
Full REST API, webhooks, and branded emails
- License key generation and management
- Stripe checkout for subscriptions, one-time payments, and trials
- SDK generator that produces a drop-in client with an auto-updater and feature gating
- Reports and analytics for MRR/ARR, license health, and seat utilization
- Automatic, license-authenticated updates
- Customer portal plus the Stripe billing portal
- Product bundles and per-tier feature gating
- Full REST API, webhooks, and branded emails
Version
10.28.0
PHP Required
7.4+
WordPress
6.0+
License
Annual
v10.28.0 · Jun 11, 2026
- Pricing: Standardized every product page's pricing cards on the Bodholdt Tickets layout. Product lines that ship a free edition (Backups and Atelier, alongside Tickets) now show the Free card first, in a four-card row, with a "Get it free" button that links to that product's free download on the same page. Bodholdt Licensing has no free edition and keeps its three cards.
- Pricing: Aligned the Free card so its feature checklist lines up with the paid cards across the row, and replaced the inline price subtext with a short "Free forever:" lead-in that mirrors the paid cards' "Everything in [tier], plus:" wording.
- Pricing: Reworked every paid tier's feature list so each one lists only what it adds over the tier directly below it, instead of repeating the lower tier's features. The full per-feature comparison table still carries the complete detail.
- Pricing: Clarified that support credits apply to one-time (Lifetime) plans only. The comparison row now reads "Support credits (Lifetime plans only)," with a note that annual and monthly plans include email support while active.
v10.27.0 · Jun 11, 2026
- Pricing: Aligned the Bodholdt Atelier pricing copy and feature comparison with Atelier 1.7.0, which moved recurring patron subscriptions (with content drip) and commissions (with milestone payments and booking) from the free Lite build to Solo and above. The Free Lite card and matrix now describe the core one-off storefront only; Solo's headline value is the two recurring/commission money-makers. Pro (Foundry, Marketplace, Publish API with the Adobe Lightroom plugin) and lifetime support credits are unchanged.
v10.26.1 · Jun 11, 2026
- Fix: On the Bodholdt Atelier product page, the "See full feature comparison" toggle rendered the Backup feature matrix instead of Atelier's. The per-product pricing block now resolves the correct Atelier comparison family.
v10.26.0 · Jun 11, 2026
- Bodholdt Atelier added to the pricing page. The /buy-plugins/ page now lists Bodholdt Atelier alongside Backups, Licensing, and Bodholdt Tickets, with a Free Lite card plus Solo, Pro, and Agency tiers, and a full feature comparison.
- Free Lite (self-hosted on your own Stripe or PayPal, 1 site, updates by manual download) includes all core commerce: the 13 work types, checkout, certificates of authenticity and provenance, per-buyer watermarking with signed expiring downloads, commissions, patrons, EU VAT and IOSS, the music pipeline, the collectors CRM, royalty splits, and the white-label storefront.
- Solo adds a license with automatic updates and direct support. Pro adds Foundry (a multi-artist collective with Stripe Connect payouts and a leaderboard), Marketplace (cross-artist discovery), and the Publish API with the Adobe Lightroom plugin. Agency is everything in Pro for up to 25 sites.
v10.25.0 · Jun 10, 2026
- Lite-edition entitlement gate (internal). Adds the runtime capability layer for a future free, capped edition. No customer-facing change for the full edition — behavior is identical to 10.24.17.
- New single source of truth (`cls_edition()` / `cls_lite_can()`): the Lite cap is opt-in via an explicit `CLS_EDITION` build constant and is hard-vetoed for any self-hosted/licensed server or recognised paid tier, so a full install can never be silently capped.
- Under Lite only: 1 product, 1 site per license; Reports, Activity Log, the management REST API, the activity-log export, and bundles are held behind the paid edition. Checkout, license issuance/validation, the customer auto-updater, the My-Keys portal, the SDK generator, and secure vault downloads always function in both editions.
v10.24.17 · Jun 10, 2026
- Free-funnel copy fixes. Removed the stale "coming soon" from the Bodholdt Tickets pricing-page section subtitle and the single-product ticketing pitch — the free (Lite) edition is live and self-served, so both now close with "Download it free."
- Cleaned up the self-serve download intro: "The free version is yours. No card, no license key." (dropped the em-dash, US spelling).
- Copy only. No payment products, pricing, identifiers, or licensing behavior changed.
v10.24.16 · Jun 10, 2026
- Bodholdt Tickets free edition is now self-served. The Bodholdt Tickets Free card on the pricing page links straight to the free download instead of showing a "Coming soon" label, pointing at the Tickets product page's free-download section.
- The self-serve email-to-download flow (and its stable free-download link) is now authorized for Bodholdt Tickets.
- No payment products, pricing, identifiers, or licensing behavior changed.
v10.24.15 · Jun 10, 2026
- Bodholdt Tickets pricing refreshed. The Bodholdt Tickets pricing cards and feature comparison now reflect the current model: a generous free tier (up to 3 agents, unlimited tickets, with Claude-BYOK AI drafting included), and three paid tiers that all unlock the same Pro features (unlimited agents, email-to-ticket piping, departments and routing, advanced reporting, and licensing-aware customer context), differing only by sites: Solo 1 site, Studio 5 sites, Foundry 25 sites.
- Free tier shown as "Coming soon." The free edition is self-served from bodholdtlabs.com and is coming soon, so its card shows a "Coming soon" label rather than a download link.
- Copy-only refresh of the pricing page. No payment products, pricing, or licensing behavior changed.
v10.24.14 · Jun 10, 2026
- "Get it free" is back on the pricing page. The backup plugins' Free card on the pricing page now links straight to the free download again (it had been a non-clickable "Coming soon" placeholder while the self-serve download was being built). It points at the matching product page's free-download section, and follows your Google Drive / OneDrive selection.
- Free download form now appears on the backup product pages. Each backup product page now shows the "Or start free with the Lite edition" download form so visitors can grab the free edition right where they're deciding.
- This release adds no payment products and changes no pricing.
v10.24.13 · Jun 10, 2026
- Free download replaces the trial. The no-card trial has been dropped — the free version *is* the trial. The self-serve sign-up no longer issues any licence: after you verify your email with a 6-digit code, we simply email you a download link for the free edition of Bodholdt Backup for Google Drive or Bodholdt Backup for OneDrive. The full Pro edition stays paid-only.
- Simpler form. The sign-up form is now a single "Get the free download" step (email → code → emailed link); the Free-vs-Trial choice is gone. The download link still arrives by email only.
- Abuse protection hardened (security review). Verification codes are now capped per email address (one per minute, a few per day) in addition to the existing per-IP limit; there is a daily ceiling on total free-download emails; and the Cloudflare visitor-IP header is now only trusted when the request genuinely arrives through Cloudflare, so the rate limits can't be bypassed by spoofing it.
- This release adds no payment products and changes no pricing.
v10.24.12 · Jun 10, 2026
- Self-serve sign-up for the backup plugins (foundation). A new public, email-verified endpoint can issue either the free Lite edition or a no-card 14-day Pro Trial for Bodholdt Backup for Google Drive and Bodholdt Backup for OneDrive, then email the license key and a download link. The visitor first requests a 6-digit code by email (using the same verification step the customer portal already uses), then enters the code and picks Free Lite or the Pro Trial.
- Built to be safe by design. The product is restricted to an allowlist (the two backup plugins), the license tier is set by the server — never by the browser — and the key and download link are sent by email only (never shown on-screen). Requesting Lite a second time re-sends your existing key rather than creating a duplicate; the 14-day Pro Trial can only be claimed once per plugin per email. The on-screen confirmation is identical whether or not you already had a license, so the form can't be used to probe which emails are customers. The endpoint is rate-limited.
- New shortcode `[cls_self_serve product="…"]` renders the two-step sign-up form for a future landing page. It is not yet placed on any page.
- This release adds no payment products and changes no pricing; the trial/Lite downloads reuse the same secure download the paid editions already use.
v10.24.11 · Jun 10, 2026
- Pricing page: the backup "Free" card's download button pointed at a WordPress.org listing that isn't published yet (it dead-ended in an empty search), and a feature bullet mentioned a WordPress.org support forum that doesn't exist. The button is now a non-clickable "Coming soon" until the free edition is available, and the forum bullet is gone.
v10.24.10 · Jun 10, 2026
- Pricing FAQ copy: the "Do you offer a free trial?" answer no longer claims a live WordPress.org free tier (the free edition isn't published yet) — it now points to the 14-day money-back guarantee. Minor punctuation/em-dash cleanup across the pricing FAQ.
v10.24.9 · Jun 10, 2026
- Customer portal polish: replaced the key emoji on the "My Keys" button with a clean inline icon that follows your brand accent colour.
v10.24.8 · Jun 5, 2026
- Admin polish: removed an off-center icon from the Generate Code button so the call-to-action reads cleanly.
v10.24.7 · Jun 5, 2026
- Admin polish: clearer button colours — store/positive actions are green, reversible "Revoke" is amber, permanent "Delete" is red, with a consistent focus ring.
v10.24.6 · Jun 5, 2026
- Admin polish: unified button styling across every admin screen so secondary buttons stay readable on the dark theme.
v10.24.5 · Jun 5, 2026
- Admin polish: the license-expiry Save button now stays beside the date field, and the License Database table no longer collides its columns on narrower screens.
v10.24.4 · Jun 5, 2026
- Admin polish: fixed the Appearance live-preview in light mode, made outline buttons readable, and tidied the Issue New License form alignment and License Database table layout.
v10.24.3 · Jun 5, 2026
- Admin polish: brightened hard-to-read dark-blue/purple text throughout the admin for better contrast on the dark theme.
v10.24.2 · Jun 5, 2026
- Admin polish: more contrast fixes on the Emails and License Database screens.
v10.24.1 · Jun 5, 2026
- Admin polish: contrast fixes across the setup checklist, dashboard, forms, and data tables on the dark theme.
v10.24.0 · Jun 5, 2026
- White-label customer checkout. Your customer-facing checkout, portal, and pricing now follow a single brand colour with neutral defaults — no Bodholdt branding imposed. A new Settings → Appearance tab lets you set the brand colour, light/dark/auto mode, corner style, and font, with a live preview of the real checkout components.
v10.23.11 · Jun 5, 2026
- Admin redesign QA: fixed an empty status chip on collapsed Products rows.
v10.23.10 · Jun 5, 2026
- Admin redesign QA: Products rows now show the status chip only when collapsed and the footer only when expanded.
v10.23.9 · Jun 5, 2026
- Admin redesign — Products. The Products screen now renders as tidy collapsible cards with a status chip, taming the densest admin page.
v10.23.8 · Jun 5, 2026
- Admin redesign — Reports. Charts, date pickers, and CSV links restyled for the dark theme and full legibility.
v10.23.7 · Jun 5, 2026
- Admin redesign — Activity Log. Action labels and the empty state restyled for readability.
v10.23.6 · Jun 5, 2026
- Admin redesign — Manage Licenses. Headers, filters, status pills, and row details restyled for the dark theme.
v10.23.5 · Jun 5, 2026
- Admin redesign — Dashboard. Stat cards, the earnings hero, and the setup checklist restyled for the dark theme.
v10.23.4 · Jun 5, 2026
- Admin redesign — Getting Started. Welcome, steps, and progress made legible on the dark theme.
v10.23.3 · Jun 5, 2026
- Admin redesign — Settings. All Settings fields and info boxes restyled for the dark theme.
v10.23.2 · Jun 5, 2026
- Admin redesign — Plugin Integration. Code blocks, the test-SDK panel, and steps restyled with clear branding.
v10.23.1 · Jun 5, 2026
- Admin redesign foundation (cont.). The main card sections, settings sub-tabs, and primary buttons adopted the new dark theme.
v10.23.0 · Jun 5, 2026
- Refreshed admin design. The licensing admin now uses the Bodholdt Labs dark design system — a new branded header and tabs, and a consistent dark canvas across every screen. Visual only; behaviour and settings are unchanged.
v10.22.6 · Jun 4, 2026
- Support routing. Help/FAQ calls-to-action now point customers to the Support page to open a ticket, instead of emailing — so pre-sales and support requests are always captured.
v10.22.5 · Jun 4, 2026
- Fix: bundles no longer show a "Free Trial" option at checkout, and a product with no trial configured no longer defaults to a 14-day trial — a missing trial length now correctly means no trial.
v10.22.4 · Jun 4, 2026
- Customers can manage their own sites. The "My Keys" view in the customer portal now lists each license's active sites (used / allowed) with a Remove button, so a customer who moves to a new domain or shuts a site down can free a slot themselves — no support email needed. The site limit itself is unchanged; this just lets customers swap which sites use it.
v10.22.3 · Jun 4, 2026
- Faster product releases. Upload (or drag-and-drop) a plugin ZIP on the Products screen and the Version, Tested up to, Requires WP/PHP, and latest changelog now auto-fill straight from the plugin — review and Save to publish. No more retyping the version after every build.
v10.22.2 · Jun 4, 2026
- Feature-comparison tables. Each product page now shows an at-a-glance feature comparison across its tiers, and the Buy Plugins page shows one per product line. All tables read from a single source so they stay in step with the pricing cards.
v10.22.1 · Jun 4, 2026
- Changelog readability. Tidied the public changelog so it reads cleanly for customers — removed internal planning and process references. No functional changes.
v10.22.0 · Jun 3, 2026
- Customer-portal integration hooks. Two new extension points let companion plugins (such as Bodholdt Ticketing) surface their own content inside your "My Keys" customer portal: `cls_portal_verified_customer_html` (append HTML to a verified customer's result) and `cls_portal_option_cards` (add a card to the portal option grid). Both are additive and no-op when nothing is hooked, so this release changes nothing on its own.
v10.21.2 · May 25, 2026
- Checkout cart polish. The checkout button's loading spinner now shows only during the in-flight request (it previously spun the whole time), and long product names no longer get truncated in the cart's product picker — the field uses the plugin font with an ellipsis and the cart row was widened and rebalanced.
v10.21.1 · May 25, 2026
- Success-page button contrast. Forced the success-page button labels to a visible color so a host theme's link color can't render them blue-on-blue.
v10.21.0 · May 21, 2026
- Plan tiers are now real. Your plan (Hobby / Studio / Foundry) is now recognized by the plugin and enforced: Reports & analytics is a Studio+ feature (Hobby sees an upgrade prompt), and Hobby is limited to one product. Your tier is read securely from your license at validation time — and if your tier can't be determined, you keep full access (no one is ever locked out of their own data). Also fixes the underlying signal so add-on plugins (e.g. the backup plugins) correctly receive their tier. Adds a `tier` column to the license table, applied automatically on update.
v10.20.0 · May 21, 2026
- Reports & analytics. A new Reports page (under the Bodholdt Licensing menu) turns the data you already have into operator insight. Revenue: MRR, ARR run-rate, ARPU, active subscriptions, refund rate, a gross-revenue trend, and MRR broken down by product — read live from Stripe (read-only), cached for an hour with a one-click Refresh. Licenses: active/expiring/lifetime counts and new-in-range, status mix, seat utilization with an "at limit" upsell signal, top products, and trial→paid conversion — all from your license table. Pick a date range (7/30/90 days, this month, or custom) and export any section to CSV. Revenue needs your Stripe key connected; the license reports work without it.
v10.19.1 · May 20, 2026
- Fun Pass follow-up. The "Issue New License" success card on the Licenses page now gets the same celebratory payoff as the dashboard's quick-create — a satisfying header, a key-reveal flourish, a bounce-in, and a confetti burst (which, like every animation in the plugin, respects your "reduce motion" setting). v10.19.0 had only upgraded the dashboard path.
v10.19.0 · May 20, 2026
- The Fun Pass — making the licensing admin feel like a game, not a chore. This release adds celebration, a sense of accomplishment over time, and a little personality to the highest-leverage moments — the first sale, issuing a license, a customer buying — without slowing any workflow. Every animated celebration respects the operating-system "reduce motion" setting (gated in JavaScript, not just CSS), and a new speaker toggle in the admin header controls celebration sounds (off by default).
- FUN-CLS-01 — First-sale celebration + lifetime revenue. The Stripe checkout webhook now accumulates a running `cls_total_revenue` total and flags the very first completed sale. The Dashboard gains an animated "Earned $X" hero stat (empty state: "$0 — your first sale is going to feel great."), and the first time a sale lands, the next Dashboard load fires a one-time confetti burst and a "Your first sale! Someone out there is running your code right now." banner.
- FUN-CLS-02 — Issuing a license now has a payoff. Quick Create on the Dashboard plays the bounce-in success animation (previously suppressed), reveals the key with a flourish, labels it "License #N minted ✦", and fires milestone confetti at the 10th, 50th, and 100th license.
- FUN-CLS-03 — Warmer customer success page. Confetti on load (skipped for reduced-motion visitors) plus more human copy — "You're in! 🎉 … let's get you up and running" — in place of the old "Payment Successful!" receipt tone.
- FUN-CLS-04 — The SDK "it's alive" moment. The SDK Generator's test result animates in with personality ("It's alive. Your plugin is now license-aware. 🔌"), and freshly generated code gets a celebratory header.
- FUN-CLS-05 — Setup-completeness meter. An animated "Setup N/7 ✓" progress bar sits above the Dashboard checklist, with a celebratory state when everything's configured.
- FUN-CLS-06 — In-character loading messages. "Creating…" / "Verifying…" rotate through friendlier lines ("Minting your key…", "Talking to Stripe…", "Conjuring your store pages…") with a rare easter-egg line. Purely cosmetic — the underlying action always fires immediately.
- FUN-CLS-07 — Warmer empty/error copy. The "My Keys" no-results message and the Dashboard activity empty state are friendlier without losing any precision.
- FUN-CLS-08 — Product "ready to sell" moment. A ready-to-sell product pill gives a green pulse and a "Ready for customers — share your store link." nudge with a one-click link to the storefront.
- FUN-CLS-09 — A guide that knows where you are. Getting Started step headings show a "✓ done" badge once their underlying config is detected (Stripe connected, products added, pages generated, emails configured), and a "You're wired up — go make a sale! 🚀" finish line appears once the core setup is live.
- FUN-CLS-10 — Celebration sound toggle. A small speaker control in the admin header stores a `cls_sound_enabled` preference (default OFF). Celebration sounds play only when it's on AND motion is allowed.
- Accessibility: all confetti and celebration animations honor `prefers-reduced-motion` via `matchMedia` (no animated pieces are generated at all for reduced-motion users), and celebration audio is gated behind both that preference and the new sound toggle.
v10.18.2 · May 20, 2026
- CC-2 closure — consistent section-header styling. The new top-level Products page (added in v10.18.0) reintroduced one raw, inline-styled `<h2>` in its empty-state hero. Swapped it onto a new `.cls-empty-hero-title` utility class in `assets/admin.css`, so the admin UI now has zero raw/inline-styled section headers. This closes the last code-track item of the internal Steve Jobs Pass; the remaining audit item (an embedded 30-second setup screencast) is a content deliverable, not code.
v10.18.1 · May 20, 2026
- Post-feature polish patch — closes the eight deferred Steve Jobs / Grandma Test audit items (NEW-09 through NEW-17).
- NEW-09 — Setup Wizard Step 3 now gates "Next" on a verified Stripe connection. Previously an operator could click straight past the Stripe step without ever verifying their key, then discover at the live checkout that payments 500. Step 3 now blocks "Next" until the key is verified (or already configured), with an inline nudge ("Please verify your Stripe connection to continue — or click 'Skip for now' to set this up later."). A new "Skip for now" affordance is the deliberate escape hatch so the wizard never traps an operator who wants to configure Stripe later.
- NEW-10 — Hardcoded `/wp-admin/` URLs swept from the wizard. The Step 5 "Add Your First Product" / "Getting Started Guide" CTAs built `window.location.origin + '/wp-admin/'`, which breaks on subdirectory WP installs (`example.com/blog/wp-admin/`). Both now use the `clsAdmin.adminUrl` base localized via `wp_localize_script` (subdirectory-correct). The "Add Your First Product" CTA also now points at the new top-level Products page (`admin.php?page=bodholdt-licensing-products`) rather than the removed `Settings → Products` sub-tab.
- NEW-11 — Setup Wizard modal is now screen-reader accessible. The wizard overlay gains `role="dialog"`, `aria-modal="true"`, and `aria-labelledby` pointing at its heading, plus a Tab/Shift-Tab focus trap and focus restoration to the triggering element on close — matching the existing `clsModal()` confirm-dialog accessibility pattern. Focus moves to the wizard on open and on each step change.
- NEW-12 — Checkout error/success banner uses the frontend CSS-variable namespace. The success branch poked `style.borderColor` / `style.color = var(--cls-success)` — an admin-namespace variable that doesn't exist on the customer-facing storefront, so the banner rendered with no color. Switched to `var(--clsf-success, #459C51)`.
- NEW-14 — Stripe SDK errors are mapped to friendly messages. The wizard's "Verify Connection", the Products-page "Create in Stripe", and Price-ID validation previously surfaced raw `\Stripe\Exception` text (e.g. `Invalid API Key provided: sk_live_...`) to the operator. A new `cls_friendly_stripe_error()` helper maps the common exception types (authentication, connection, permission/restricted-key, rate-limit, invalid-request) to plain-language guidance; the raw message is still written to the error log for debugging.
- NEW-15 — Getting Started Guide copy updated for the current admin IA. Step 1 said "Settings → Stripe & License" (renamed to just "Stripe" back in v10.14.3); the product/version links pointed at the removed `Settings → Products` sub-tab. Both now read correctly and link to the top-level Products page.
- NEW-16 — Customer-portal billing lookup no longer leaks customer type. The "manage billing" lookup returned three different success messages depending on whether the email was a Stripe subscriber, a trial/one-time customer, or unknown — letting an attacker enumerate customer status. All three branches now return one identical message that still surfaces the "My Keys" guidance to everyone. The billing-portal email is still sent only to genuine Stripe customers.
- NEW-17 — Verification-code error message hardened. The lookup-code verify response is uniform whether the email is unknown, the code is wrong, or the code has expired, so it can't be used to confirm an account exists.
- Maintenance: README `Stable tag` brought current (was stuck at 10.17.2; backfilled the 10.18.0 changelog entry below). Guarded a `json_decode(null)` PHP 8.3 deprecation notice in the Licenses table when a license has no registered domains.
v10.18.0 · May 19, 2026
- HEADLINE — Products promoted to a top-level admin tab. Products moved from a `Settings → Products` sub-tab to its own top-level page at `admin.php?page=bodholdt-licensing-products` (between Manage Licenses and Activity Log). Operators configure products repeatedly over a plugin's life, unlike the configure-once Stripe / License / Email settings; burying Products under Settings demoted it relative to the wizard's "Add Your First Product" CTA. The empty-state hero, inline Stripe Product-creation panel, product cards, version-management section, and JS templates all moved across faithfully.
- NEW — first-class product bundles. A `cls_products` entry can now be a *bundle*: two optional fields, `grants_products` (an array of component product IDs) and `tier_override` (a tier key applied to each component license), turn one purchase into multiple license keys. The checkout webhook already issued multiple licenses natively — v10.18.0 adds the buy-URL expansion (`cls_resolve_bundle_payload()`) and the webhook tier-override read (`cls_resolve_component_tier()`) so each component license is issued at the chosen tier (e.g. both backup plugins at the 5-site Pro tier) rather than each component's root Solo tier. Graceful fallback if a tier doesn't exist on a component; cycle prevention so a bundle can't grant itself.
- Operator UX: a per-card "This is a bundle" toggle reveals a component multi-select (pre-filtered to non-bundle products) plus a tier-override picker, hides the ZIP/trial/sites/version fields that don't apply to bundles, and shows a BUNDLE badge in the card header. New helpers: `cls_product_is_bundle()`, `cls_resolve_bundle_payload()`, `cls_get_product_by_id()`, `cls_resolve_component_tier()`.
- Build-pipeline hardening: the build script now excludes `.fuse_hidden*` / `._*` / `.DS_Store` cruft from the shipped ZIP at both the rsync and ZipArchive layers.
- Backwards-compatible: every change is `function_exists()`-gated and new metadata defaults via `??`, so pre-v10.18.0 checkout sessions hit the legacy path unchanged.
v10.17.2 · May 19, 2026
- Pricing-page logic patch — three regressions surfaced post-v10.17.1.
- Fix: per-tier prices now actually differ. `cls_resolve_tier()` was referenced by the pricing-page shortcode since v10.9 but never defined anywhere. The `function_exists()` check fell through to `$tier = $product` (root product fields), so every tier_key returned the same root prices and all three tier cards on Bodholdt Licensing's Hobby/Studio/Foundry (and each backup plugin's Solo/Pro/Agency) showed identical prices regardless of which tier the customer picked. Fix: implement `cls_resolve_tier()` in `helpers.php` to read `$product['tiers'][$tier_key]` if present, returning the root entry for `'default'` tier. Operator action required: populate the `tiers` array in `cls_products` data via WP Admin → Bodholdt Licensing → Settings → Products. Without that, all cards still display the root price (this patch only fixes the algorithm).
- Fix: Buy buttons now actually go to Stripe Checkout. The pricing page generated `?cls_buy=plugin&tier=pro&interval=yearly` URLs but no handler intercepted them anywhere in the plugin — clicking Buy loaded the homepage with the params hanging in the URL bar. Fix: new `cls_handle_buy_url()` hooked to `init` priority 5 in `class-cls-pricing-page.php` validates params, looks up the Stripe Price ID via `cls_resolve_tier`, creates a Stripe Checkout Session (subscription mode for monthly/yearly, payment mode for onetime), and redirects to Stripe's hosted checkout. Sets `cls_order_payload` metadata so the existing `checkout.session.completed` webhook in `class-cls-api.php` issues the license normally. Failure states redirect to `/buy-plugins/?cls_buy_error=<reason>` for operator diagnosis.
- Fix: comparison chart now has room to breathe. Widened the pricing-page container `clspp-wrap` from 1180 to 1340px so the whole page gets more horizontal room. Comparison-table cell padding bumped from 9×12 to 12×16, font-size 0.9rem → 0.95rem, line-height 1.5, `min-width` 600 → 800, plus first column gets a 38% width allocation so multi-word feature labels like "Streaming restore (FK-safe, BLOB-safe)" stop wrapping. All sub-sections inside `.clspp-wrap` inherit the wider container so the page stays visually consistent top-to-bottom.
v10.17.1 · May 19, 2026
- Site bug patch — three regressions surfaced during v10.17.0 Verification B.
- Fix: `[cls_pricing_page]` + `[cls_pricing_block]` shortcodes now actually register. `includes/class-cls-pricing-page.php` existed in the working tree but was never `require_once`'d by the bootstrap AND was untracked in git, so the file never shipped to prod and the shortcodes never loaded. Result on the live site: `/buy-plugins/` rendered the shortcode as literal text, every `/software/<plugin>/` product detail page had an empty buy-CTA section (no Purchase button anywhere). Fix: add the require to `custom-licensing-system.php`'s licensed-modules block + ensure the file is git-tracked so it deploys with the plugin.
- Fix: theme `bl_starting_price()` no longer returns the lowest of monthly/yearly/onetime when callers want a yearly price. Previously, Solo backup with `monthly_price=3.99 / yearly_price=29 / onetime=69` returned `$3.99` and the homepage card template rendered it as `$3.99 / yr` (since the period is hardcoded). Same bug on Bodholdt Licensing's Hobby tier (`$5.99/mo` showed as `$5.99/yr`). New algorithm: prefer the lowest `yearly_price` across the product's root fields AND any nested `tiers`. Fall back to `onetime_price` (lifetime — acceptable on a /yr card as a single-purchase alternative). Last resort: `monthly_price × 12` (annualized). Function now matches the marketing intent every caller expects. Theme file `themes/bodholdt-labs/functions.php` (~30 LOC swap).
- Post-ship requirement: clear the `bl_starting_price_*` transient cache so the new price values render immediately. `wp transient delete --all` covers it; the ship procedure script does this.
v10.17.0 · May 19, 2026
- HEADLINE — Stripe auto-create from the Products tab. Replaces the manual "go to Stripe dashboard, create a Product, create monthly/yearly/lifetime Prices, copy three `price_…` IDs, paste them back in this form" loop with one in-plugin step. Click *Create new product in Stripe*, enter Name + Description + the prices you want, and the plugin calls `\Stripe\Product::create()` + up to three `\Stripe\Price::create()` calls behind the scenes, then drops a pre-filled Product card into your list with the returned IDs. Manual entry remains available via *Advanced — use existing Stripe products* for operators with existing Stripe inventory. New AJAX endpoint `cls_stripe_create_product` (manage_options + admin nonce gated). Includes partial-create rollback: if Product creation succeeds but a Price creation fails, the orphaned Product is archived (`active: false`) so Stripe Dashboard doesn't accumulate junk.
- Email correctness pass (NEW-01 / NEW-02 / NEW-13). Adds new `cls_send_mail()` helper that wraps `wp_mail()` with a branded `From: <Brand> <noreply@host>` header (operator-overridable via the new `cls_email_from_name` / `cls_email_from_address` / `cls_email_reply_to` options), a `Reply-To:` header pointing at the operator's admin email by default, and a plain-text `AltBody` via `phpmailer_init` for multi-part email correctness. Migrates every customer-facing send through the new helper: trial-checkout, paid-purchase webhook, verification-code lookup, billing-portal access link, admin payment-failure notification, daily expiry reminder, "Email License to Customer" admin action, and Send Test Email. Fixes the silent template-discard bug where the trial-checkout and paid-purchase webhook paths built the operator-customized `cls_email_trial_body` / `cls_email_purchase_body` template into a local variable and then never used it — real customers received a hardcoded license-block instead of the customized template (the Send Test Email path correctly used the template, so operators tested green and shipped broken). v10.17.0 routes both paths through the new `cls_render_email_from_template()` helper which substitutes `{licenses}`, `{email}`, `{manage_url}` into both HTML and plain-text bodies.
- First-time-user-experience FTUE pass (NEW-06 / NEW-07 / NEW-08). Adds `plugin_action_links_*` filter so the plugin row on `wp-admin/plugins.php` now surfaces Setup Wizard / Settings / Getting Started links next to Deactivate (was previously bare — operators had to hunt the new sidebar item themselves). Adds a one-shot post-activation admin notice — "Bodholdt Licensing activated. Run the 5-minute Setup Wizard to connect Stripe and create your first product. [Start Setup Wizard] [Open Getting Started Guide] [Skip for now]" — that auto-dismisses on first dismiss or after 30 minutes, suppressed on Bodholdt Licensing's own admin pages, gated on `manage_options`. Adds smart-default storage path (`wp-content/uploads/cls-vault`) auto-created at activation with Apache 2.4+ `Require all denied` `.htaccess` + an HTTP 403 `index.php` for defense-in-depth — Dashboard checklist now starts green on Day 1 instead of showing a red ✗ with no way for the operator to know what path to type.
- Settings hardening (NEW-03 / NEW-05). Stripe Secret Key and Webhook Signing Secret save handlers now surface explicit errors when the key prefix doesn't match — previously, pasting a restricted key (`rk_…`) or a publishable key (`pk_…`) silently failed while the page still said "Settings Saved." Now the operator sees "Stripe Secret Key was not saved — it must start with sk_test_ or sk_live_. Restricted keys (rk_…) and publishable keys (pk_…) are not yet supported." with a hint pointing at the right Stripe dashboard section. License-key placeholder text standardized to `XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX` (4×8 hex) across the Setup Wizard, Settings → License, and the SDK Generator test panel, matching the actual format `cls_format_license_key()` returns (was previously a mix of 4×4 and 8×4 examples that didn't match real keys).
- P3-E — Customer Portal email autofill. When a customer completes checkout, a 60-day `cls_recent_purchase_email` cookie is set via JS on the success page render (using `SameSite=Lax` and the `Secure` flag on HTTPS). On the Customer Portal's next visit, the plugin renders a "We recognize this email from a recent purchase: [email protected] — [Continue with this email] / [Use a different email]" banner above the form, pre-fills the email input, and lets the customer skip retyping. Does NOT bypass the verify-code / billing-portal authentication flow — purely a UX shortcut.
- CC-3 — Keyboard shortcuts (progressive disclosure). Press `?` on any Bodholdt Licensing admin page to reveal an overlay listing the available shortcuts. `g` followed by `d`/`l`/`s`/`g`/`k`/`a` navigates to Dashboard / Licenses / Settings / Guide / SDK Generator / Activity log (vim-style). On the Licenses table specifically, `j`/`k` move row selection up/down, `e` triggers Edit, `r` Revoke, `d` Delete. Suppressed while typing in inputs / textareas / selects / contenteditable elements; suppressed when meta/ctrl/alt are held. Esc closes the overlay or clears row selection. Scoped to plugin admin pages only via body-class + DOM-marker detection so shortcuts don't leak into the rest of WP admin.
- Theme polish — bodholdt-labs CSS-variable injection. The `bodholdt-labs` cyberpunk-dark theme's `.cls-portal-wrapper` / `.cls-checkout-wrap` block at line 1924+ now opens with a CSS-variable injection block that maps the theme's tokens (`--bg-card`, `--text-primary`, `--neon-cyan`, etc.) INTO the plugin's v10.16.0 `--clsf-*` namespace. The legacy `!important` overrides remain in place as belt-and-suspenders until the plugin migrates remaining structural rules (padding, layout) to variables — both apply simultaneously without conflict. Cleaner integration; no functional change at this stage.
- NEW-04 — Empty-state for Products tab. When no products are configured, the Products sub-tab now shows a friendly hero callout: "Add your first product. Each product is one plugin or theme you sell. The fastest path is to let Bodholdt Licensing create the Stripe Product and Prices for you in one step — no need to leave this page. [Create new product in Stripe (recommended)] [Advanced — use existing Stripe products]". Previously the panel rendered three buttons (Add Product / Save All / Generate Pages) over a blank area with zero copy — a non-developer had no idea which to click first. The redundant Save / Generate buttons are hidden until at least one product exists.
- clsAdmin.adminUrl localized via `wp_localize_script` so the wizard's CTAs + keyboard shortcuts build correct paths on subdirectory WP installs (`example.com/blog/wp-admin/`). Full NEW-10 hardcoded-URL sweep deferred to v10.18.x.
v10.16.0 · May 19, 2026
- NEW: assets/frontend.css — a new dedicated stylesheet for the three customer-facing shortcodes (cls_checkout_form, cls_success_page, cls_portal). Previously these embedded their CSS via 3 inline `<style>` blocks + 76 inline `style="..."` attrs scattered across `includes/class-cls-frontend.php`. v10.16.0 extracts all sweepable CSS into one well-organized stylesheet that loads on any page containing one of the 3 shortcodes via `wp_enqueue_style`. The 10 remaining inline-style attrs are all intentional: 7 in HTML email templates (where clients like Gmail / Outlook strip external stylesheets), 2 in SVG path-specific animations tied to stroke-dasharray for the success-page checkmark draw-in, 1 false-positive grep match in a ship-note comment. Effectively 100% of sweepable inline styles removed.
- NEW: theme-agnostic CSS-variable architecture. All colors, typography, spacing, border-radius, and shadow values in frontend.css are CSS variables on `:root` (`--clsf-bg`, `--clsf-text`, `--clsf-primary`, `--clsf-accent`, `--clsf-border`, `--clsf-radius-md`, etc.). Host themes can re-skin the customer-facing shortcodes by declaring their own variable values in a scoping selector — no `!important` patches needed. Example for a dark-themed host: `.my-dark-theme .cls-wrapper, .my-dark-theme .cls-portal-wrapper, .my-dark-theme .cls-success-wrapper { --clsf-bg: rgba(255,255,255,0.03); --clsf-text: #f0f0f5; --clsf-border: rgba(255,255,255,0.08); }`. Defaults are a clean light-mode design using the official Bodholdt Labs brand palette — works on any WordPress theme (Astra, Twenty Twenty-Four, Storefront, custom).
- NEW: ~50 utility classes added to assets/frontend.css covering all 3 customer-facing shortcodes. Checkout: `.cls-wrapper`, `.cls-form-section`, `.cls-label`, `.cls-input`, `.cls-row`, `.cls-price-display`, `.cls-price-free`, `.cls-remove-btn`, `.cls-add-btn`, `.cls-checkout-btn`, `.cls-btn-spinner`, `.cls-totals`, `.cls-grand-total`, `.cls-total-amount`, `#cls-error-banner`, `.cls-error-banner-dismiss`, `#cls-confirm-box` + nested classes, animations (`cls-spin`, `clsSlideIn`, `clsPriceFlash`, `clsModalEnter`), `.cls-stripe-attribution`, mobile media query. Success page: `.cls-success-wrapper`, `.cls-success-icon`, `.cls-success-title`, `.cls-success-sub`, `.cls-success-downloads` + title/hint, `.cls-license-card` + `.cls-license-card-download`, `.cls-success-expired` + title/body, `.cls-success-whatsnext` + title/list, `.cls-success-actions` + `.cls-success-btn-primary/secondary`, animations (`cls-success-enter`, `cls-circle-draw`, `cls-check-path`). Portal: `.cls-portal-wrapper`, `.cls-portal-title`, `.cls-portal-intro`, `.cls-portal-field`, `.cls-portal-options`, `.cls-portal-option-card` + meta, `.cls-portal-btn`, `.cls-portal-secondary`, `.cls-verify-step`, `.cls-verify-prompt`, `.cls-verify-row`, `.cls-verify-btn`, `.cls-resend-btn`, `.cls-verify-help`, `.cls-wrong-email-link`, `.cls-portal-msg` + `--success`/`--error` variants, `.cls-portal-banner` + `--success`/`--error` variants, `.cls-license-result` + `.cls-lr-meta`/`-form`/`-download`/`-empty`, `.cls-results-box` + `-title`, `.cls-skeleton-row` + shimmer animation, `.cls-card-block` + `--warning` variant for empty-state / maintenance cards.
- REFACTORED: enqueue logic — previously `wp_register_style('cls-checkout-styles', false)` then `wp_add_inline_style` injected ~80 lines of CSS at runtime. Replaced with proper `wp_enqueue_style('cls-frontend', ..., CLS_VERSION)` so the browser can HTTP-cache the stylesheet and version-bust on plugin update via the existing CLS_VERSION cache-buster. Load condition expanded: previously checked only for `cls_checkout_form`; now loads on any of `cls_checkout_form`, `cls_success_page`, or `cls_portal` (success + portal pages used to inherit the inline `<style>` blocks within their own shortcode output).
- Closes CC-4 (Customer-facing portal page styling decoupled from cls-admin brand) from our internal design review. The customer portal at `/manage-subscription/` and its companion shop / success pages no longer ship as a tangle of inline styles — they share one cohesive stylesheet that's themeable, cacheable, and shippable to WP.org.
- HOST-THEME NOTE: the `bodholdt-labs` cyberpunk-dark theme that powers `bodholdtlabs.com` (where these shortcodes render in production) has existing `!important` overrides on `.cls-portal-wrapper` / `.cls-checkout-wrap` at theme line 1924+. Those overrides still work as-is post-v10.16.0; a future ship can refactor them to use the new CSS variable injection pattern for cleaner theme integration.
v10.15.2 · May 18, 2026
- NEW: page-guide.php inline-styles swept — 89 → 0 (100% reduction). Every inline `style="..."` attribute removed. ~12 new utility classes added to cls-admin.css for the Getting Started Guide page: welcome banner (`.cls-guide-wrap` / `.cls-guide-welcome` / `.cls-guide-welcome-title` / `.cls-guide-welcome-sub`), progress card (`.cls-guide-progress-card` / `.cls-guide-progress-head` / `.cls-guide-progress-label` / `.cls-guide-progress-meta` / `.cls-guide-progress-track` / `.cls-guide-progress-fill`), reset button (`.cls-guide-reset-btn`), per-step header pattern (`.cls-guide-step-header` already existed; added `.cls-guide-checkbox` for the 18px green-accented checkbox and `.cls-guide-step-num` base + 6 color modifiers `--blue/green/purple/orange/teal/lime` for the numbered circles — lime variant uses navy text for contrast, the rest use white), `.cls-h2-flush` for `margin:0` H2 inside the step header, `.cls-guide-list` for the line-height:2 ordered lists, `.cls-link-cyan` for brand-cyan anchors (heavily reused), `.cls-info-box-grey-sm` + `.cls-info-box-grey-fs13` variants for the 4 grey-on-grey info boxes inside steps, `.cls-page-grid-3` + `.cls-page-card-grey` + `.cls-page-card-meta` + `.cls-page-card-code` for the Step-3 three-column grid (responsive: collapses to 1col @ ≤700px), `.cls-copy-wrap-md` + `.cls-input-mono-grey` for the webhook URL copy widget, `.cls-table-bare` + `.cls-table-label-col` + `.cls-table-label-col-200` for the Quick Reference widefat table, `.cls-faq-item` + `.cls-faq-q` + `.cls-faq-a` for the 6 FAQ entries. v10.15.2 also added `.cls-guide-sublist` (`list-style:disc; padding-left:20px; margin:4px 0`) for Step 4's nested ul.
- NEW: page-settings.php inline-styles swept — 81 → 1 (98% reduction). Only the dynamic vault-indicator div retains an inline `style` because the background / border / color values are PHP-computed at runtime from the `$is_secure_basement` boolean + the `$status_color` variable. Everything else moved to utility classes: License sub-tab intro sub-text + license-key input + status fw-semibold span, Stripe sub-tab help-cyan note (replacing inline `background:#f0f8ff; border-left:3px solid #00BCF2; ...`) + link-cyan-bold + form-input-full (Secret + Webhook Secret inputs) + help-cyan-note-sm (the in-form light-blue test/live-mode description) + cls-text-red (Not configured states) + cls-mt-sm (verify-button rows) + verify-status + webhook-test-status + cls-input-mono-grey-full (the webhook URL + storage-path inputs) + cls-status-msg-ok/warn/err (Folder Found / not-writable / not-found spans) + cls-vault-indicator (the dynamic-color status panel) + cls-currency-select + cls-page-slug-label + cls-page-slug-input. Emails sub-tab: cls-placeholder-btn-mono (the `{licenses}` `{email}` `{manage_url}` buttons) + cls-email-h3-flex (Reset-to-Default flex header) + cls-fw-semibold (Subject/Body labels) + cls-email-subject-input + cls-email-body-textarea + cls-email-actions-row + cls-email-preview-mt. Products sub-tab: cls-products-actions (top button row) + cls-btn-cyan-primary (Add Product) + cls-card-h3-white + cls-card-num-faded + cls-card-name-white (the dark card header) + cls-file-found / cls-file-missing (✓/✗ vault-file status) + cls-mt-sm (upload zone) + cls-product-grid-2 (Trial Days + Sites Allowed 2-col layout) + cls-field-help-flex (the 3 "Where do I find this? / Validate" rows on Monthly/Yearly/Lifetime price IDs) + cls-btn-validate + cls-card-field-mt + cls-changelog-textarea + cls-actions-row-mt + cls-add-product-btn. Branding + Advanced: cls-brand-name-input + cls-checkout-btn-input + cls-h2-navy (Settings Export/Import + Admin Notices headers) + cls-export-row + cls-import-file-mr + cls-desc-mt + cls-notice-form-row + cls-notice-form-fields + cls-notice-msg-col + cls-notice-label-block + cls-notice-msg-input + cls-btn-clear-red + cls-notice-list-mt + cls-notice-item-pad + cls-desc-mt-sm. Bonus: the Admin Notices section's `style="border-top:3px solid #FF8C00;"` got replaced with the existing `data-border="orange"` attribute now that v10.15.2 extends data-border to all brand colors.
- NEW: cls-section[data-border] attribute extended to ALL brand colors. Pre-v10.15.2 only blue/green/purple/orange/navy were supported as `data-border` values; teal/lime/red/pink/yellow had to be inlined as `border-top:3px solid #XXXXXX;`. v10.15.2 adds the missing 5 selectors so every brand color is reachable via the same attribute pattern: `data-border="teal|lime|red|pink|yellow"`. page-guide.php Step 5 (teal) + Step 6 (lime) + page-settings.php Admin Notices section (orange) now use the attribute instead of inline.
- CUMULATIVE phase 1+2+3 reduction across all 6 admin pages: 317 → 26 inline styles (92% reduction). Per-file: page-dashboard.php 34 → 5 (85%), page-activity.php 26 → 10 (62%), page-generator.php 37 → 2 (95%), page-licenses.php 50 → 8 (84%), page-guide.php 89 → 0 (100%), page-settings.php 81 → 1 (98%). All 26 remaining are intentional: dynamic PHP-computed values (vault-indicator colors, grace-period bar fill, status pill color, file-found state) + table-column widths (contextual to specific table layouts where extracting to classes would obscure rather than help). The cyberpunk-dark refresh inline-styles sweep (CC-1) is now COMPLETE across all admin surfaces.
v10.15.1 · May 18, 2026
- NEW: page-generator.php inline-styles swept — 37 → 2 (95% reduction). New utility classes added for the SDK Generator: info-card pattern (Generator Instructions + What This Does panels), SDK step headers (Step 1/2/3), SDK code blocks (dark navy/lime for generated SDK + light grey for init snippet), prefix-preview callout (v10.14.1 P1-E), Test SDK input row + result states (info/ok/fail/err — v10.14.4 P3-D), `<details>` summary + pre styling. Remaining 2 inline styles: 1 `display:none` for the test-result div initial state (could class but JS toggles inline anyway) + 1 `margin-top:30px` on the outer post-submit wrapper (one-off).
- NEW: page-licenses.php inline-styles swept — 50 → 8 (84% reduction). New utility classes added for the Licenses page: Issue New License card + form + field-flex layouts (extends v10.15.0's existing `cls-create-license-card` pattern), expiry quick-pick chips (`.cls-chip` + `.cls-chip-row`), Issue success / error state cards, license-creation-disabled warning card, License Database gradient header + lime records-badge, license search-bar + bulk-actions row, pagination text + button row, success-notice green-border + new-key inline-flex copy-wrap + send-key-to-customer button alignment. Remaining 8 inline styles are ALL table-column widths (3% / 22% / 18% / 14% / 12% / 7% / 8% / 13%) — contextual to the licenses-table layout, intentionally inline. Preserved all v10.14.1 P1-D AJAX success card + P2-E txn_id details + P2-G admin-email pre-fill + P3-B expiry chips.
- NEW: ~25 additional utility classes added to cls-admin.css for SDK Generator + Licenses-page-specific patterns. Categories: info-card pattern (cls-info-card / cls-info-card-navy / cls-info-card-green / cls-info-card-grid), SDK code blocks (cls-sdk-code-dark / cls-sdk-code-light), SDK step headers (cls-sdk-step-header / cls-sdk-step-header-mt), prefix-preview box (cls-prefix-preview-box / cls-prefix-preview-code), Test SDK states (cls-test-input-row / cls-test-key-input / cls-test-result / cls-test-result-info/ok/fail/err / cls-test-details / cls-test-details-summary / cls-test-details-pre), Issue New License layout (cls-issue-license-card / cls-issue-license-form / cls-issue-field / cls-issue-field-fixed / cls-issue-success / cls-issue-error / cls-issue-h2 / cls-issue-h2-orange), expiry chips (cls-chip / cls-chip-row), disabled-card (cls-disabled-card / cls-disabled-meta), License Database header (cls-page-h1-gradient / cls-records-badge), notice-border extension (cls-notice-green-border), width utility (cls-w-full), form section card (cls-form-section-card), Licenses filter/bulk/pagination (cls-license-search-input/select/form / cls-license-filter-wrap / cls-license-bulk-row/select / cls-pagination-text / cls-pagination-buttons), code-actions row (cls-code-actions), and a handful of misc helpers (cls-inline-flex-auto / cls-license-key-lg / cls-ml-md / cls-ml-auto / cls-no-products-warn).
- VISUAL CHANGE ADVISORY: the License Database gradient `<h1>` header at the top of the Licenses page is now powered by `linear-gradient(135deg, var(--cls-navy), var(--cls-purple))` which evaluates to the v10.15.0 re-anchored navy `#00188F` (was inline `#00188F` already — exact match, no visual change here). The lime records-badge inside the H1 uses `var(--cls-lime)` (`#BAD80A`, brand-exact). The SDK Generator's Step 1 generated-code textarea is now `background: var(--cls-navy)` with `color: var(--cls-lime)` — was hardcoded `#00188F`/`#BAD80A` inline, so visual is identical except the navy is now uniform across the admin (no more drift between this code-block and other navy-using components).
v10.15.0 · May 18, 2026
- NEW: CSS-variable palette re-anchored to the official Bodholdt Labs brand. Pre-v10.15.0 the cls-admin.css `:root` palette had drifted off-brand on 3 colors (--cls-navy was #0B1467 instead of brand #00188F; --cls-green was #2d9d4e instead of brand #459C51; --cls-orange was #e68a00 instead of brand #FF8C00). The drift was historical accident — the older hand-typed inline-style hex codes scattered through the admin pages were ON-brand all along; only the newer "cyberpunk-dark" CSS-variable palette drifted. v10.15.0 re-anchors --cls-navy, --cls-green, --cls-orange to the brand values, and adds TWO new variables (--cls-yellow #FDF250, --cls-pink #D82E8A) plus their `-soft` background variants and an explicit --cls-black. Components in cls-admin.css currently using `var(--cls-navy)` / `var(--cls-green)` / `var(--cls-orange)` will visually re-tint to the brand shades — back to brand-correct.
- NEW: 30+ utility classes added to cls-admin.css for the v10.15.0 admin inline-styles sweep. Text colors (cls-text-navy / cls-text-cyan / cls-text-green / cls-text-orange / cls-text-red / cls-text-purple / cls-text-muted / cls-text-faint / cls-text-meta), font sizes (cls-text-xs / cls-text-sm / cls-text-md), font weights (cls-fw-bold / cls-fw-semi), margin utilities (cls-mt-xs/sm/md/lg/xl + cls-mb-xs/sm/md/lg/xl + cls-m-0), flex layout primitives (cls-flex-row / cls-flex-row-md / cls-flex-row-end / cls-flex-between / cls-flex-wrap / cls-stat-row), brand-tinted buttons (cls-btn-green + cls-btn-green-lg modifier extending the existing cls-btn-cyan / cls-btn-navy), status pill (cls-status-pill with dynamic background staying inline), info boxes (cls-info-warn + cls-info-warn-text), code block (cls-code-block), bare card (cls-card-bare), quick-action group (cls-quick-action-group), mono small (cls-mono-sm), form helpers (cls-form-label-block / cls-form-input-flex), dashboard checklist details (cls-check-detail-toggle / cls-check-detail-meta / cls-check-action-row), empty-cta utilities (cls-empty-cta / cls-empty-cta-icon / cls-empty-cta-title / cls-empty-cta-desc), responsive 2-col grid (cls-dash-2col with cls-empty modifier), plus a few one-offs (cls-self-end / cls-hidden). Sets the foundation for v10.15.0a (page-generator + page-licenses) and v10.15.0b (page-guide + page-settings) which will reuse + extend these classes.
- NEW: page-dashboard.php inline-styles swept — 34 → 5 (85% reduction). Remaining 5 are justifiable: 2 dynamic PHP values (grace-period bar fill, status pill background color), 3 table-column widths (contextual to the recent-activity table layout).
- NEW: page-activity.php inline-styles swept — 26 → 10 (62% reduction). Remaining 10: 5 table-column widths, 1 dynamic PHP value, 4 minor spacing one-offs.
- VISUAL CHANGE ADVISORY: components on every admin page that use --cls-navy / --cls-green / --cls-orange will subtly re-tint from the pre-v10.15.0 shades to brand-correct shades. This is intentional, not a regression. If a specific component looks off after deploy, the fallback is to re-anchor that ONE component's CSS rule to a legacy color rather than reverting the whole CSS-var change. Worst case: revert the v10.15.0 commit and stay on v10.14.4 until the visual issue is resolved.
v10.14.4 · May 18, 2026
- NEW: Getting Started Guide progress now syncs across devices. Pre-v10.14.4, each step's done-state was stored in browser `localStorage` — meaning an operator who marked 3 of 6 steps complete on their Mac saw 0 of 6 from their iPad the next day, and there was no way to know whether they'd actually done the work or just forgotten which browser they used. v10.14.4 persists progress to per-user WP meta (`cls_guide_completed_steps`) via a new `cls_guide_progress_set` AJAX endpoint, so the operator's progress travels with them across devices, AND multi-operator stores don't clobber each other (per-user, not per-install). One-time localStorage → user-meta migration runs on first load if the operator had pre-v10.14.4 state in their browser. Reset progress button now clears both the in-memory state and the server-side meta.
- NEW: SDK Generator "Verify It Works" Step 3. After clicking Generate Code, operators now see a third section below Step 1 (Create The File) + Step 2 (Hook It Up): paste a license key, click 🧪 Test SDK, and a new `cls_test_generated_sdk` AJAX endpoint simulates the exact `cls_check` call the generated SDK would make against the live server via `wp_remote_post`. Returns a green "✓ SDK works." with the license's expiry + domain-registration status on success, or a red "✗ SDK check failed." with the server's error message + raw JSON response (expandable for debugging) on failure. Closes the loop in the same admin session — operator can validate the SDK actually resolves real license keys before committing the code to their plugin.
- Audit P3-F (version badge in admin shell) WITHDRAWN on baseline inspection. A `<span class="cls-version-tag">v10.14.x</span>` badge already exists at `class-cls-admin.php:161` inside the `cls_render_admin_tabs()` function — it appears on every Bodholdt Licensing admin page next to the brand header. The internal design review was wrong; no code change needed. Same correction pattern as v10.14.1 P3-C, v10.14.3 P2-C/P2-D.
v10.14.3 · May 18, 2026
- NEW: Settings sub-tab "Stripe & License" split into separate "License" + "Stripe" panels. Pre-v10.14.3 the first sub-tab housed THREE unrelated concerns crammed onto one screen: (1) License Status (your Bodholdt Licensing home-license key + grace-period state), (2) Stripe & Payment Configuration (Secret Key + Webhook + Currency + Page Slugs + Storage Path), (3) a Storage vault security indicator. Steve Jobs Pillar 3 ("every screen should have ONE primary action that visually dominates") was violated by having 2-3 primary actions per screen. v10.14.3 splits the navigation: License is now its own sub-tab (primary action: paste + save license key), Stripe keeps the Stripe & Payment Configuration form intact (primary action: paste + Verify Connection). The Storage Path field + its contextual vault-security indicator stay paired inside the Stripe form (moving them to Advanced would orphan the indicator from the input it describes — the audit's original suggestion was reconsidered on inspection). Also removed dead-code re-definition of `$is_secure_basement` / `$status_color` / `$status_msg` (5 vestigial lines that re-set variables already defined near the top of `cls_settings_page()`).
- NEW: Stripe sub-tab inline "Show Step-by-Step Setup Guide" disclosure replaced with a single source-of-truth link. Pre-v10.14.3, a `<button>` toggle revealed a nested 4-step list inside the Stripe sub-tab — duplicating the same content already maintained on the dedicated Getting Started Guide page (`/wp-admin/admin.php?page=bodholdt-licensing-guide`). Two sources of truth drift over time. Now: a single inline blurb *"Need help connecting Stripe? See the [Getting Started Guide →] for the full 4-step walkthrough"* on a soft-blue background. The Guide page is the canonical reference; the Settings page links to it.
- NEW: Dashboard "Plugin License" Configure button now points to the dedicated License sub-tab. Was `#cls-sub-stripe` (pointing at the old combined Stripe & License sub-tab); now `#cls-sub-license` reflecting the new split. The other 3 Dashboard sub-tab anchors (Stripe Secret Key, Stripe Webhook Secret, File Storage) continue to point at `#cls-sub-stripe` because all 3 fields live in the Stripe form panel.
- Audit P2-C (Email-template live preview) and P2-D (Send Test Email button) WITHDRAWN on baseline inspection. Both were ALREADY shipped in the pre-v10.14.0 code: `clsUpdateEmailPreview()` at admin.js:823-862 does full token substitution + binds to `input` events on subject/body fields, and `clsSendTestEmail()` at admin.js:611 plus the Send Test buttons at page-settings.php:370 + 392 plus the `cls_send_test_email` AJAX endpoint at class-cls-ajax.php:48 form a complete circuit. Both defects don't exist in production. Same lesson as v10.14.1 P3-C: when an audit hypothesizes missing infrastructure, grep for the corresponding window-bound function or AJAX endpoint before claiming the defect.
v10.14.2 · May 18, 2026
- NEW: Setup Wizard Step 1 welcome copy rewritten for non-developer operators. Pre-v10.14.2 opener: *"This wizard will help you configure your license server in a few simple steps. You'll need your Stripe API keys ready."* — assumed the operator already had a Stripe account + located their API keys. ~80% of small-business owners considering Bodholdt Licensing have neither. New copy: *"We'll walk you through everything in about 5 minutes — paste your license key, connect Stripe (we'll show you how if you don't have an account yet), and create the three pages your customers will use to buy and manage their subscriptions. No technical experience needed."* Plus a small reassurance below: *"First time using Stripe? We'll help you set it up in Step 3."*
- FIXED: Setup Wizard Step 2 "Save & Next" no longer silently advances on AJAX failure. Pre-v10.14.2, if the license-key + currency save AJAX failed (network blip, server-side rate-limit, license-server outage), the wizard advanced anyway to Step 3 — operator thought their data saved when it didn't. Now: on AJAX failure, an inline error appears in the existing `#cls-wiz-step2-status` div ("Save failed — please try again." or "Network error — check your connection and try again.") and the wizard does NOT advance. Operator can correct + retry.
- NEW: Setup Wizard step-progress labels gracefully degrade on narrow viewports. Pre-v10.14.2, the 5-step progress bar ("Welcome / License / Stripe / Pages / Done") wrapped awkwardly on tablet-portrait widths or in collapsed-sidebar WP Admin layouts. Now: `@media (max-width: 640px)` hides the labels and relies on the existing `title` attribute for tap-tooltip discoverability. Step bars get slightly thicker (6px → 8px) on narrow viewports to compensate for the removed labels.
- NEW: Setup Wizard Step 4 explains what each generated page is for. Pre-v10.14.2, the body said *"We'll create three WordPress pages for your store: a checkout page, a success page, and a customer portal. You can customize them later."* Then a tiny 12px grey footer line listed the page names. Now: an explicit bullet list inside the body shows each page's URL slug + a one-line role description (e.g. */buy-plugins* — your storefront / */purchase-success* — the thank-you page after checkout / */manage-subscription* — where customers look up their keys, cancel, or upgrade). Operator knows what they're creating BEFORE clicking Generate Pages.
- NEW: Setup Wizard Step 5 has one primary CTA, not two competing ones. Pre-v10.14.2, Step 5 had a callout box pointing at the Getting Started Guide AND a "Add Your First Product →" primary footer button — two CTAs visually competing for attention. Steve Jobs Pillar 3: every screen should have ONE primary action that dominates. The Guide link is now a small "Need more help?" footer affordance below the primary CTA, properly subordinated.
v10.14.1 · May 18, 2026
- NEW: Issue New License is now AJAX-driven with a satisfying success card. Pre-v10.14.1, generating a manual license submitted a form, reloaded the page, and the new license was just another row in the table below — you had to scroll, find your new row, select the key text, copy it, and email it to the customer. v10.14.1 intercepts the form submit, fires the `cls_quick_create_license` AJAX (which already existed), and renders a green confirmation card inline: *"✓ License created!"* with the formatted key in a copy-button-paired code block, *"Emailed to <[email protected]> ✓"*, and two action buttons — Issue another (keeps the email pre-filled because operators typically issue multiple keys to the same person) and View all licenses ↓ (page reload to see the new row in the table). Non-JS fallback path is preserved — the legacy POST handler still runs if JavaScript is disabled.
- NEW: Issue New License pre-fills the operator's admin email. Pre-v10.14.1, the User Email field had `[email protected]` as a placeholder (visual only, not actually pre-filled). Now the field is pre-populated with `wp_get_current_user()->user_email` (falling back to the `admin_email` option). Operators issuing comp licenses or replacements to themselves no longer retype. Clear the field to type a different recipient as before.
- NEW: Issue New License has expiry quick-pick chips. "Never", "30 days", "90 days", "1 year" — one click sets the date picker to today + N days (or clears it for Never). Covers the common operator workflows (apology comp, beta period, lifetime grant) without clicking through the date picker.
- NEW: SDK Generator shows a live class-name preview as you type. Type "BodholdtBackup" in the Unique Prefix field and a blue callout below appears: *"This will generate the class: `BodholdtBackup_Licensing_Client`"*. Updates on every keystroke. Sanitization matches the server-side normalization so the preview matches the actual generator output. Non-developer operators can validate their prefix before clicking Generate Code.
- NEW: SDK Generator has a "what this does, in plain language" panel. Side-by-side with the existing Generator Instructions card, a green-bordered explanation panel: "We'll write you a PHP file that connects your plugin to your Bodholdt Licensing server. It checks license keys, blocks unlicensed installs from auto-updating, and shows your customers a friendly setup screen. Drop the file into your plugin's includes/ folder and add one require_once line — we'll show you exactly what. Total install time: about 2 minutes. No prior SDK knowledge needed." De-risks the page for first-time customers.
- NEW: Licenses table hides raw Stripe txn_id behind a `<details>` affordance. Pre-v10.14.1, every subscription row showed `Sub: sub_1TWmZHG7DhP9pK2L...` as plain grey text below the license key. That ID is useful exactly once (when an operator needs to look up a subscription in the Stripe dashboard to diagnose a payment issue). The rest of the time it was visual noise on a high-density row. Now collapsed behind a *"Stripe details"* expander; click to reveal the `txn_id` + a *"Open in Stripe →"* deep-link to the Stripe Dashboard (auto-routes to test-mode dashboard for `sub_test_*` subscriptions). Same treatment for the raw product-slug shown below the product name — now behind a *"Slug"* disclosure.
- Note on P3-C from the Steve Jobs audit. The internal design review proposed adding skeleton loaders to the Licenses-table Sites column during a per-row AJAX domain-count load. On code inspection, the Sites count is rendered server-side from the `registered_domains` DB column at page-render time, NOT via AJAX — so there's no AJAX call to add a skeleton to. P3-C was based on a wrong reading of the code and is dropped from this ship. (Audit fidelity lesson: verify the assumption when you go to ship.)
- Copy-button confirmation flash on the SDK Generator's post-submit Step 1 + Step 2 code blocks (P1-E sub-item) is auto-handled via the existing `.cls-copy-btn` class wired in `assets/admin.js` — no code change needed in this ship, just verified the flash animation triggers correctly on the generator's output buttons.
v10.14.0 · May 18, 2026
- NEW: Customer Portal Steve Jobs polish — verify-step echoes back your email. Pre-v10.14.0, when a customer clicked "My Keys" on the Manage Subscription portal page, the generic prompt *"Enter the 6-digit code sent to your email"* appeared with no echo-back of which email the code was actually sent to. A customer who typed their email wrong didn't realize until they sat waiting for a code that never arrived. v10.14.0 rewrites the prompt to read *"We sent a 6-digit code to [email protected] — check your inbox!"* (the bold email is inserted via DOM `createTextNode` so customer-supplied text can't inject markup). Also adds a "Wrong email?" affordance that re-shows the email input on click, in case the customer notices the mistake from the echo-back. Refreshes the same echo-back when the customer clicks "Resend Code", so support tickets for "I never got the email" become diagnose-yourself.
- NEW: Customer Portal Steve Jobs polish — skeleton loaders during the verify-code AJAX. Pre-v10.14.0, after submitting the 6-digit code, the customer waited 1-4 seconds (depending on connection + server) staring at a button that just said "Verifying..." with no visual progress in the results area. v10.14.0 renders 3 shimmering skeleton rows in the results area during the verify AJAX call — same width/height/border-radius as the final license-result rows that replace them. Customer perceives the page as "working on it" instead of "frozen". Uses a single CSS `@keyframes cls-skeleton-shimmer` (200% background-size + linear-gradient → animated background-position) — no external dependencies, ~12 lines of CSS, 0 JS frameworks. Skeleton rows are `aria-hidden="true"` so screen readers don't announce three empty regions during the wait. On AJAX failure, skeletons are cleared so the error message stands alone.
- Note on version-number gap (10.5 → 10.14.0): the plugin has shipped many intermediate versions (10.11.x, 10.12.x, 10.13.0 — multisite license-storage migration) since the last readme.txt update. Those entries were structurally shipped via the version constant + plugin header. v10.14.0 catches up the readme to current. Future versions append here normally.
v10.5 · Feb 26, 2026
- AUDIT: Comprehensive 9-phase commercial-grade self-audit
- SECURITY: 32 fixes — sanitized all superglobals, escaped all output
- SECURITY: Replaced date() with wp_date(), json_encode() with wp_json_encode()
- SECURITY: Added sanitize_callback to all 18 register_setting() calls
- SECURITY: Replaced short echo tags for PHP short_open_tag compatibility
- SECURITY: License key no longer exposed in JavaScript (boolean flag only)
- NEW: "Preserve data on delete" option in Settings > Advanced
- NEW: Contextual help tabs on all admin screens
- IMPROVED: Dashboard query caching with 5-minute transient
- IMPROVED: autoload=false on infrequently-accessed options
- IMPROVED: PHP 8.0 runtime version check on activation
v10.0 · Feb 25, 2026
- REFACTORED: Monolithic admin class (2,291 lines) split into 7 focused modules
- NEW: includes/admin/ directory with dedicated page files for each admin screen
- NEW: Admin bootstrap dispatcher (184 lines)
- IMPROVED: Inline styles moved to admin.css (Content Security Policy ready)
- IMPROVED: Brand header and version tag use proper CSS classes
v9.0 · Feb 25, 2026
- IMPROVED: Full WordPress Coding Standards compliance
- NEW: i18n-ready with complete .pot file for translations
- NEW: GPL-2.0-or-later LICENSE file included in distribution
- NEW: Distribution build script (build.sh) with critical file verification
- NEW: Directory browsing guards (index.php) on all directories
- IMPROVED: cls_format_license_key() centralized in helpers.php
v8.1 · Feb 24, 2026
- SECURITY: Fixed grace period loophole — 7-day activation deadline enforced
- SECURITY: Settings import can no longer override operating mode
- SECURITY: Stripe secret keys excluded from settings export
- SECURITY: REST API GET /licenses/{key} now returns minimal data for public requests
- SECURITY: License key lookup now requires email verification (6-digit code)
- SECURITY: Removed @unserialize fallback (all data migrated to JSON)
- SECURITY: X-Forwarded-For header no longer trusted (Cloudflare IP only)
- SECURITY: .htaccess protection for download storage directory
- NEW: max_allowed_domains configurable per product in Product Manager
- NEW: invoice.payment_failed webhook handler with activity logging
- NEW: Production-quality SDK Generator with auto-updater, deactivation, and feature gating
- IMPROVED: Consolidated API init hooks into single dispatcher
- IMPROVED: Email delivery failures logged to activity log
- IMPROVED: Updated README documentation for v8.x architecture
- FIX: Version alignment across all files (8.1)
- FIX: .DS_Store files removed
v8.0 · Feb 24, 2026
- NEW: Licensed Self-Hosted operating mode (phone-home verification)
- NEW: Soft lockout system — only blocks new license creation on expired license
- NEW: Auto-updates from Bodholdt home server
- NEW: Simplified admin UI — single operating mode, clean license key section
- NEW: Self-referential licensing (server licenses itself)
- IMPROVED: Operating mode radio buttons removed from UI
v7.0 · Feb 24, 2026
- NEW: Self-Hosted / Client dual operating mode
- NEW: Auto-updater system for client installations
- NEW: Activity logging with audit trail
- NEW: Settings export/import
- NEW: Bulk license operations
- NEW: REST API endpoints
- NEW: Configurable currency
- NEW: Configurable page slugs
- NEW: Webhook URL auto-display
- NEW: Admin notification system
- NEW: Stripe version conflict detection
- NEW: Multi-file plugin architecture
- NEW: WordPress-standard README and documentation
- NEW: i18n text domain support
- NEW: uninstall.php for clean removal
- NEW: Deactivation hook for transient cleanup
- NEW: PHPUnit test stubs
- IMPROVED: Gatekeeper bypassed in self-hosted mode (no chicken-and-egg problem)
- IMPROVED: All page slug references use saved options
- 20 commercial readiness improvements
v6.4
- 24 UI/UX and accessibility fixes
- Inline error banners replacing alert()
- Order confirmation flow
- Loading spinner
- Pagination and search for license management
- License key lookup in customer portal
- Trial user portal messaging
- WCAG 2.1 status badges
v6.3
- 10 security fixes including path traversal prevention
- Proxy-aware rate limiting
- Webhook elseif chain optimization
- Strict comparisons throughout
v6.2
- 16 security fixes
- JSON domain storage (replaced PHP serialize)
- Webhook idempotency guard
- CORS restriction to known origins
- Stripe lazy loading
v6.1
- 12 security fixes
- CSRF protection on all forms
- XSS prevention with proper escaping
- SQL injection prevention with prepared statements
- Rate limiting system
After purchase, you'll receive a download link and license key by email. In your WordPress admin, go to Plugins → Add New → Upload Plugin, choose the ZIP you downloaded, click Install, then Activate. The plugin will prompt you for your license key on its settings page. Paste it in and you're live.
A single WordPress install with a unique domain. Subdomains, staging clones, and dev environments don't count against your site limit as long as they're flagged as non-production in the plugin settings.
Yes. Stripe pro-rates the difference. If you bought Hobby in January and upgrade to Studio in April, you pay the prorated difference between Studio and Hobby for the remaining 8 months. Lifetime-to-lifetime upgrades just pay the difference.
At renewal, yes. Pick a lower tier when your license is up. We don't refund the difference for mid-cycle downgrades.
Lifetime means lifetime of the product. You pay once, you get updates and your license stays active forever. Lifetime licenses also include a bucket of support credits that never expire, so you always have a way into support. If we ever sunset a product (announced 12 months in advance), lifetime customers get a free transfer to the equivalent successor.
Yes. Stripe handles cancellation in one click from your account dashboard. You keep access until the end of the billing period.
14 days, no questions asked, full refund. Open a ticket on our Support page (bodholdtlabs.com/support) using the email you bought with.
Two reasons: Stripe's per-transaction fees compound when you charge 12 times instead of once, and monthly subs have higher churn risk for us. Annual or lifetime is the better deal if you know you're sticking around.
Hobby ($49/yr) licenses 1 product on 1 site. Studio ($99/yr) unlocks unlimited products, 5 sites, and the full reporting dashboard for revenue, MRR, license health, and seat utilization. Foundry ($199/yr) adds unlimited sites. All three include license keys, the auto-updater, the customer and Stripe billing portals, and the SDK generator.
There's no separate free trial, but every plan has a 14-day money-back guarantee, so you can buy it, wire it into your store, and refund within 14 days if it doesn't fit.