feat: moonbase_licensing JUCE 8 module with native activation UI#14
Merged
Conversation
Drop-in JUCE module (modules/moonbase_licensing) that talks to the Moonbase licensing API natively (not juce::OnlineUnlockStatus) with a built-in, brandable activation UI: online + offline activation, license details, deactivation, and online re-validation to refresh entitlements after a purchase. - Zero third-party deps: juce::WebInputStream transport, vendored nlohmann/json, OS-native RS256 verification (Security.framework / CNG / libcrypto) behind a compile-time backend selector that defaults to OpenSSL, so existing SDK consumers are unchanged. - Core SDK (gated, additive): detail/crypto backends, seat-count claims, validate_token_local_allow_expired, MOONBASE_DISABLE_CURL_TRANSPORT guard. - Quality of life: fail-loud config validation, an onDiagnostic sink, opt-in JUCE analytics/telemetry metadata, and expired-offline-license cleanup. - Build/test/CI: MOONBASE_USE_CURL / MOONBASE_SANITIZER options, native sample app, offscreen UI snapshot harness (Argos), JUCE controller test suite, and CI running the suites and sanitizers across macOS/Linux/Windows. The raw SDK target is renamed moonbase_cpp; the moonbase::licensing alias is preserved.
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
Silence -Wshadow (StyledButton/LinkButton ctor params shadowed juce::Button::text) and -Wsign-conversion (url_encode iterated a string as unsigned char). Behavior unchanged; tests still green.
…erialization) - refreshLicense: persist on the message thread, gated by the generation check and serialized against deactivate()/clearLicense(), so an in-flight refresh can no longer recreate a just-cleared license on disk. The throttled path now passes a should_persist predicate to suppress the SDK's background-thread write. - der.hpp: bounds-check the TLV length against the remaining buffer before advancing, so a short-form length larger than the input is a configuration error instead of a read past the decoded key (Apple/Windows backends). - types.hpp: serialize seat_count / seats_used in the license JSON round trip so stored licenses keep their seat data. Tests: +2 JUCE (resurrection guard, malformed-DER rejection), +1/updated core (seat round trip). Core 57/57, JUCE 20/20.
choco install openssl flakes intermittently ('Value cannot be null'). Use the
vcpkg toolchain like ci.yml does for its Windows deps, which also auto-deploys
the OpenSSL DLLs next to the test executable.
An inline run: starting with a quoted scalar broke the workflow parse; use a run: | block like ci.yml does.
Routing keyed the trial screen on config.enableTrial, so a backend-granted trial fell through to the generic license-details view when the merchant had the 'Start trial' button disabled (e.g. the sample app). Decouple them: enableTrial now only governs the Welcome 'Start trial' affordance, while any active trial license routes to the trial view (applyLicense + showDetails via a shared screenForCurrentLicense() helper).
…ts in ActivationConfig Adds onlineCheckInterval (5 min default), onlineGracePeriod (7 days), and httpConnectTimeout/httpRequestTimeout to ActivationConfig, plumbed through toLicensingOptions(). These were the remaining licensing_options fields the module didn't surface (only target_platform stays auto-detected).
Instead of dropping an ended trial straight to the Welcome screen, route it to a new Expired view (from the design's "Trial expired" state): muted brand lockup, a red "Trial expired" pill, the end date, a full red bar, an "audio is bypassed" note, "Unlock full version", and "Activate offline instead". The plugin stays locked: license() remains empty (DSP gating keeps bypassing), while the ended trial is held in expiredTrial_ for display only. start() detects an expired trial on load and routes via showTrialExpired(); setPreviewState handles Screen::Expired the same way. Tests: controller test (expired trial -> Expired + license locked) and a 06b-trial-expired visual snapshot. JUCE 23/23, core 57/57.
Previously only a locally-expired trial showed the Expired screen; a trial that was still valid locally but expired per an online re-validation fell through to the locked/Welcome path. Now both start() and refreshLicense() catch license_expired_error from re-validation and, for a trial, route to the Expired screen (holding the trial we have, since the throw returns no token) while keeping the plugin locked. Non-trial expiry and network blips are unchanged. Tests: +2 JUCE (start() re-validates expired -> Expired; refreshLicense expired -> Expired + locked). JUCE 25/25.
The native module can't initiate a trial (trials are granted by the backend; there is no SDK call to start one), and the button's onClick just ran the same online-activation flow as "Activate online". Remove the button outright along with the now-pointless config.enableTrial flag, strings.startTrial / startTrialText(), and the trial mention in the default welcome copy. trialLengthDays + trialFeatures stay (the Trial / Expired screens display them). JUCE 25/25; the welcome snapshot is unchanged (the demo already had it off).
…rflow The features were drawn as fixed rows straight onto the view, so a long list ran under the buttons / clipped and a short list clustered at the top. Move them into a juce::Viewport with a TrialFeatureList content component: rows are evenly spaced and centred vertically when they fit, and the viewport scrolls (vertical scrollbar) when they overflow. Adds a 06c-trial-overflow visual snapshot.
Neither was read anywhere: the 'Manage license' button that consumed manageUrl was removed earlier, and supportUrl was never wired up. Remove the dead fields from ActivationConfig and the docs that advertised them, so the config surface only exposes options that do something.
Previously start()/poll/deactivate/refresh ran on detached juce::Thread::launch workers that were never joined, so a worker could keep executing module code (a blocking WebInputStream read, up to the connect timeout) after the controller - and, during plugin scanning / pluginval, the plugin binary - was destroyed. The WeakReference/generation guards prevented controller use-after-free but not this. Now: - juce_http_transport builds its WebInputStream directly and exposes cancel() (WebInputStream::cancel) to interrupt a blocking read from another thread. - ActivationController runs all async work on an owned juce::ThreadPool; the destructor calls the transport's cancel() then removeAllJobs(wait), so workers are unblocked and joined before anything is destroyed - no detached thread outlives the controller, and the drain is near-instant. Consumers can now call start() straight from the editor ctor and tear down at any time without deferring it. Adds a test that destroys the controller while a request is blocked and asserts it cancels + drains without hanging (JUCE 26/26).
Add licensing_options::client_info, a free-form token the base client appends to the User-Agent after "moonbase-cpp/<version>", so the server can tell which higher-level client made the request. The JUCE module fills it via toLicensingOptions() with "moonbase-juce/<version> (JUCE <v>; <os>)". Also define MOONBASE_LICENSING_VERSION / MOONBASE_CPP_VERSION in the module header: the SDK version macro only reached the moonbase_cpp target, so the module was reporting moonbase-cpp/0.0.0. Now it reports the real version. Tests: client_tests asserts client_info is appended to the UA; controller_tests assert the module fills client_info and that a real request carries moonbase-cpp/<ver> (not 0.0.0) + moonbase-juce/. Core 57/57, JUCE 27.
…rflow When the trial features are longer than the available band, draw a bounded, bordered field and inset the viewport inside it, with an always-visible, higher-contrast scrollbar (no autohide), so it unmistakably reads as scrollable. A list that fits still renders as a plain centred checklist per the design. Also leave breathing room between the feature field and the Unlock button. Verified via the 06c-trial-overflow visual snapshot (and 06-trial for the fits case). JUCE 27/27.
…, gate, app version) Acting on integration feedback: 1. ActivationComponent(ActivationController&) — a non-owning overload so the processor and editor share one controller (one license.mb, no hand-rolled reloadLicense() re-sync). The owner calls start(); the component reflects the controller's current state. 2. ActivationController::licensedFlag() — a lock-free std::atomic<bool> updated on the message thread (via a centralized setLicense()), so processBlock gating is `if (! controller.licensedFlag().load()) ...` with no ChangeListener. 3. LicenseGate — a dependency-free, click-free audio gate (short fade on activate/deactivate) the consumer drives; the module still never silences audio. 4. ActivationConfig::toLicensingOptions() auto-fills application_version from JucePlugin_VersionString in a plugin (no JUCEApplication to read it from), so telemetry "just works". Tests: licensedFlag transitions, component sharing one controller, LicenseGate fade/clear behavior. JUCE 30/30 (146 assertions); sample + snapshots build clean.
…ures get room The subtitle used a fixed 40px block (top-aligned) with a 14px gap before the progress bar, so a one-line subtitle left a large gap above the bar and squeezed the feature list. Compute the trial layout in one place (paint + viewport share it) with a subtitle sized to its wrapped height and tighter gaps, so the bar sits right under the text and the feature band gets the reclaimed space. Also drop the always-on scrollbar (setAutoHide(false)) that showed a stray scrollbar even when the list fit; the default autohide shows it only on overflow (still high-contrast + framed when it does). JUCE 30/30; snapshots verified.
Add snapshots for a subscription license (details with a real Expires date, not "Never") and the three error states the UI surfaces: a failed activation request (Welcome/Error), an invalid offline response file (Offline), and a deactivate that couldn't reach the server (Details). setPreviewState now routes its error string to the field the target screen renders (offlineError for Offline, statusMessage otherwise) so these states are previewable; covered by a controller test. Snapshot README table refreshed. JUCE 31/31.
TobbenTM
added a commit
that referenced
this pull request
Jun 26, 2026
…#17) PR #14 added @argos-ci/cli to package.json devDependencies but did not regenerate package-lock.json. The Release workflow (.github/workflows/release.yml) runs 'npm ci', which exits non-zero when the lockfile is out of sync, so semantic-release never ran and no release has been cut since v3.1.0. Regenerate the lockfile to add @argos-ci/cli and its transitive deps. Verified with 'npm ci --dry-run' (exit 0).
github-actions Bot
pushed a commit
that referenced
this pull request
Jun 26, 2026
# [3.2.0](v3.1.0...v3.2.0) (2026-06-26) ### Bug Fixes * sync package-lock.json so the release workflow's npm ci succeeds ([#17](#17)) ([8f606b3](8f606b3)), closes [#14](#14) ### Features * actionable HTTP and license-file permission errors ([#16](#16)) ([91523c2](91523c2)) * moonbase_licensing JUCE 8 module with native activation UI ([#14](#14)) ([2c434a6](2c434a6)), closes [hi#contrast](https://github.com/hi/issues/contrast)
|
🎉 This PR is included in version 3.2.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
modules/moonbase_licensing, a drop-in JUCE 8 module that integrates the Moonbase licensing API natively (notjuce::OnlineUnlockStatus) with a brandable built-in activation UI covering online/offline activation, license details, deactivation, and online re-validation to refresh entitlements after a purchase. It ships zero third-party dependencies (JUCE WebInputStream transport, vendored nlohmann/json, OS-native RS256 verification via Security.framework/CNG/libcrypto) behind a compile-time backend selector that defaults to OpenSSL, so existing SDK consumers are unchanged. Core SDK changes are additive and gated: a crypto backend abstraction, seat-count claims,validate_token_local_allow_expired, and a curl-transport guard; the raw CMake target is renamedmoonbase_cppwith themoonbase::licensingalias preserved. It also adds fail-loud config validation, anonDiagnosticsink, opt-in JUCE telemetry metadata, expired-offline-license cleanup, a native sample app, an offscreen UI snapshot harness (Argos), and a JUCE controller test suite. New CMake options (MOONBASE_USE_CURL,MOONBASE_SANITIZER, JUCE build flags) and CI run the test suites and ASan/TSan across macOS, Linux, and Windows.