Skip to content

feat: moonbase_licensing JUCE 8 module with native activation UI#14

Open
TobbenTM wants to merge 14 commits into
mainfrom
juce-module-packaging
Open

feat: moonbase_licensing JUCE 8 module with native activation UI#14
TobbenTM wants to merge 14 commits into
mainfrom
juce-module-packaging

Conversation

@TobbenTM

Copy link
Copy Markdown
Member

Adds modules/moonbase_licensing, a drop-in JUCE 8 module that integrates the Moonbase licensing API natively (not juce::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 renamed moonbase_cpp with the moonbase::licensing alias preserved. It also adds fail-loud config validation, an onDiagnostic sink, 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.

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.
@argos-ci

argos-ci Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) 🔵 Orphan build (Review) 11 added Jun 25, 2026, 6:39 AM

TobbenTM added 13 commits June 24, 2026 13:22
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant