diff --git a/README.md b/README.md index 56b9511..67316bf 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ The build provides three options, all useful when consuming the SDK as a subproj | --- | --- | --- | | `MOONBASE_BUILD_TESTS` | `ON` for the top-level project, `OFF` as a subproject | Build the doctest-based unit and live tests. | | `MOONBASE_BUILD_EXAMPLES` | `ON` for the top-level project, `OFF` as a subproject | Build the standalone activation example under `examples/`. | -| `MOONBASE_BUILD_JUCE_EXAMPLE` | `OFF` | Fetch JUCE and build the JUCE bridge example (see below). | +| `MOONBASE_BUILD_JUCE_EXAMPLE` | `OFF` | Fetch JUCE and build the JUCE `OnlineUnlockStatus` bridge example (see below). | +| `MOONBASE_BUILD_JUCE_NATIVE_EXAMPLE` | `OFF` | Fetch JUCE and build the `moonbase_licensing` native module example (see below). | Override `MOONBASE_BUILD_TESTS` and `MOONBASE_BUILD_EXAMPLES` explicitly when you want a subproject integration to build SDK artifacts too. @@ -212,20 +213,102 @@ native provider and `` for the default CURL transport. ## JUCE Plugins -For JUCE-based plugins and applications, the SDK ships a drop-in bridge -([`docs/juce.md`](docs/juce.md)) that wires Moonbase activation into -`juce::OnlineUnlockStatus`, sources the device fingerprint from JUCE's +For JUCE-based plugins and applications there are two integration paths, both +built on the same `moonbase::licensing` core SDK. The native +[`moonbase_licensing`](docs/juce-module.md) module is the recommended choice for +new projects; the [`juce::OnlineUnlockStatus` bridge](docs/juce.md) remains +available and unchanged. + +| | Native module | `OnlineUnlockStatus` bridge | +| --- | --- | --- | +| **Form** | Drop-in JUCE module | Copy-paste reference header | +| **Built-in UI** | Yes (polished, animated, themeable) | No (you build it) | +| **JUCE integration** | Native Moonbase API | `juce::OnlineUnlockStatus` wrapper | +| **JUCE version** | 8.0.4+ | 7+ | +| **Third-party deps** | None (JUCE `WebInputStream` HTTP, bundled `nlohmann/json`, OS-native RS256) | Inherits the core SDK's CURL + OpenSSL | +| **Entry point** | `ActivationComponent` / `ActivationDialog` | `MoonbaseUnlockStatus` | +| **Best for** | New plugins wanting a ready-made UI | Apps already on `OnlineUnlockStatus`, or JUCE 7 | +| **Docs** | [`docs/juce-module.md`](docs/juce-module.md) | [`docs/juce.md`](docs/juce.md) | + +### Native module: `moonbase_licensing` + +A drop-in JUCE 8 module that adds Moonbase activation, plus a built-in +themeable activation UI, to any app or plugin. It talks to the Moonbase +licensing API natively (it does not use `juce::OnlineUnlockStatus`) and has no +third-party dependencies: HTTP goes through `juce::WebInputStream`, JSON is a +bundled `nlohmann/json`, and RS256 verification uses the OS-native crypto +(Security.framework, CNG/bcrypt, libcrypto). The module lives at +[`modules/moonbase_licensing/`](modules/moonbase_licensing/); add it with +`juce_add_module()` (or via Projucer), fill in three config fields, and show one +`ActivationComponent`. See [`docs/juce-module.md`](docs/juce-module.md) for the +full guide. + +

+ The moonbase_licensing activation UI showing license details: licensed-to name, email, plan, activation type, expiry, seat count, and a Deactivate this device button. +

+ +Build the in-repo sample app with: + +```bash +cmake -B build -DMOONBASE_BUILD_JUCE_NATIVE_EXAMPLE=ON +cmake --build build --target MoonbaseActivationNative +``` + +#### Reference implementation: DRIFT + +[**DRIFT by Corino**](https://github.com/Moonbase-sh/corino-drift) is a JUCE 8 +VST3 / AU / Standalone plugin built as a reference implementation of this module +(the native-module counterpart to [HALO](https://github.com/Moonbase-sh/corino-halo), +which uses the bridge). Its knobs are real, automatable parameters that +deliberately don't process audio; the point is the activation workflow wrapped +around a real plugin: + +- The processor owns the headless `ActivationController` as the single source of + truth and calls `start()` to load + validate any stored license on a background + thread, updating the audio-thread-safe `licensedFlag()`. +- `LicenseGate` reads that lock-free flag in `processBlock` and fades to silence + when unlicensed (and back up when licensed), so gating never clicks. +- The editor shares the processor's controller and shows the module's brandable + `ActivationComponent` as a modal overlay (`overlayBackdrop = true`), opened from + a "Manage License" button via `appear()`; `onActivationChanged` keeps the UI in + sync with no re-wiring. +- Every connection, branding, trial and telemetry field lives in one + `makeDriftActivationConfig()` factory shared by the processor and the editor. +- GitHub Actions CI + release pipelines build the plugin bundles across platforms. + +DRIFT consumes the module via `FetchContent` + `juce_add_module()` (no git +submodule). See +[`src/Licensing.h`](https://github.com/Moonbase-sh/corino-drift/blob/main/src/Licensing.h) +for the single config point and its +[`CMakeLists.txt`](https://github.com/Moonbase-sh/corino-drift/blob/main/CMakeLists.txt) +for the full wiring. + +### `OnlineUnlockStatus` bridge + +A drop-in bridge ([`docs/juce.md`](docs/juce.md)) that wires Moonbase activation +into `juce::OnlineUnlockStatus`, sources the device fingerprint from JUCE's `SystemStats` helpers, and populates activation metadata with host/system -context (DAW, plugin format, OS, CPU, JUCE version). The bridge header lives -at [`examples/juce/MoonbaseJuceBridge.h`](examples/juce/MoonbaseJuceBridge.h) -and is copy-pasteable into any JUCE project. +context (DAW, plugin format, OS, CPU, JUCE version). The bridge header lives at +[`examples/juce/MoonbaseJuceBridge.h`](examples/juce/MoonbaseJuceBridge.h) and is +copy-pasteable into any JUCE project; you supply your own activation UI. + +Build the in-repo sample app with: + +```bash +cmake -B build -DMOONBASE_BUILD_JUCE_EXAMPLE=ON +cmake --build build --target MoonbaseJuceExample +``` -### Reference implementation: HALO +The flag is opt-in: JUCE is fetched and compiled only when it's set (the same +applies to `MOONBASE_BUILD_JUCE_NATIVE_EXAMPLE`). + +#### Reference implementation: HALO [**HALO by Corino**](https://github.com/Moonbase-sh/corino-halo) is a JUCE 8 standalone GUI application built specifically as a reference implementation of this SDK. It's a saturator-styled app that doesn't actually process -audio — the entire point is the license-gate workflow around it: +audio; the entire point is the license-gate workflow around it: - Startup runs a synchronous local JWT check, then re-validates against the Moonbase API on a background thread via `tryLoadStoredLicenseAsync`. @@ -238,22 +321,11 @@ audio — the entire point is the license-gate workflow around it: publishes binaries straight to a Moonbase tenant. HALO vendors the bridge header verbatim from this repo and consumes -`moonbase::licensing` via `FetchContent` — see +`moonbase::licensing` via `FetchContent`. See [`src/license/HaloLicenseBridge.cpp`](https://github.com/Moonbase-sh/corino-halo/blob/main/src/license/HaloLicenseBridge.cpp) and its [`CMakeLists.txt`](https://github.com/Moonbase-sh/corino-halo/blob/main/CMakeLists.txt) for the full wiring. -### Building the in-repo example - -A smaller standalone example also ships in this repository: - -```bash -cmake -B build -DMOONBASE_BUILD_JUCE_EXAMPLE=ON -cmake --build build --target MoonbaseJuceExample -``` - -The flag is opt-in — JUCE is fetched and compiled only when it's set. - ## Live Tests Unit tests do not hit the network. Live API tests are opt-in: diff --git a/assets/moonbase-juce-license.png b/assets/moonbase-juce-license.png new file mode 100644 index 0000000..c613646 Binary files /dev/null and b/assets/moonbase-juce-license.png differ diff --git a/assets/moonbase-juce-trial.png b/assets/moonbase-juce-trial.png new file mode 100644 index 0000000..dd280f9 Binary files /dev/null and b/assets/moonbase-juce-trial.png differ diff --git a/modules/moonbase_licensing/README.md b/modules/moonbase_licensing/README.md index 451098a..f2928f4 100644 --- a/modules/moonbase_licensing/README.md +++ b/modules/moonbase_licensing/README.md @@ -4,6 +4,13 @@ License activation for JUCE 8 apps and plugins, with a polished built-in UI, in drop-in [JUCE module](https://github.com/juce-framework/JUCE/blob/master/docs/JUCE%20Module%20Format.md). Add the module, fill in three fields, show one component. +

+ Trial screen: a free-trial panel with days remaining, a progress bar, and an Unlock full version button. + License details screen: licensed-to name, email, plan, activation type, expiry, seat count, and a Deactivate this device button. +

+ ## Features - **Native integration.** Talks to the Moonbase licensing API directly: online diff --git a/modules/moonbase_licensing/juce/ActivationConfig.h b/modules/moonbase_licensing/juce/ActivationConfig.h index bbd75c2..6214934 100644 --- a/modules/moonbase_licensing/juce/ActivationConfig.h +++ b/modules/moonbase_licensing/juce/ActivationConfig.h @@ -88,6 +88,11 @@ struct ActivationConfig bool overlayBackdrop = false; // dim the host behind the panel (modal over a plugin) instead of a full opaque backdrop int trialLengthDays = 14; // trial length shown on the Trial / Expired screens (trials are granted by the backend, not started from the UI) + // Optional override for the device name shown on the activation screen (the + // " · " chip). When empty, the OS hostname is used + // (juce::SystemStats::getComputerName, via the default fingerprint provider). + juce::String deviceName; + // Product / manufacturer brand mark shown in the header lockup. When unset, // a generated sun mark in the accent colour is drawn. Provide a vector // Drawable (e.g. juce::Drawable::createFromSVG(...)) for crispness: diff --git a/modules/moonbase_licensing/juce/ActivationController.cpp b/modules/moonbase_licensing/juce/ActivationController.cpp index 4ed83d6..3b84fd3 100644 --- a/modules/moonbase_licensing/juce/ActivationController.cpp +++ b/modules/moonbase_licensing/juce/ActivationController.cpp @@ -48,7 +48,9 @@ ActivationController::ActivationController(ActivationConfig config) } cancelInFlight_ = [transport] { transport->cancel(); }; - setDeviceLabel(juce::String(fingerprint->device_name())); + setDeviceLabel(config_.deviceName.isNotEmpty() + ? config_.deviceName + : juce::String(fingerprint->device_name())); } ActivationController::ActivationController(ActivationConfig config, @@ -684,14 +686,20 @@ void ActivationController::setPreviewState(Screen screen, std::optional now) +{ + previewClock_ = now; +} + //============================================================================== int ActivationController::trialDaysRemaining() const { if (! license_ || ! license_->expires_at) return 0; + const auto now = previewClock_.value_or(std::chrono::system_clock::now()); const auto seconds = std::chrono::duration_cast( - *license_->expires_at - std::chrono::system_clock::now()) + *license_->expires_at - now) .count(); if (seconds <= 0) return 0; diff --git a/modules/moonbase_licensing/juce/ActivationController.h b/modules/moonbase_licensing/juce/ActivationController.h index eb73717..dc9b42f 100644 --- a/modules/moonbase_licensing/juce/ActivationController.h +++ b/modules/moonbase_licensing/juce/ActivationController.h @@ -10,6 +10,7 @@ // license() and repaint. #include +#include #include #include #include @@ -97,6 +98,12 @@ class ActivationController : private juce::Timer, juce::String previewError = {}, bool busy = false); + // Pin the wall clock used for trial-countdown math (trialDaysRemaining), so + // previews and snapshot tests render a fixed number of days regardless of the + // real date. Affects display only; no effect on the live activation flow. Pass + // nullopt to unpin and fall back to the system clock. + void setPreviewClock(std::optional now); + //== Accessors ============================================================= [[nodiscard]] Screen screen() const noexcept { return screen_; } [[nodiscard]] const std::optional& license() const noexcept { return license_; } @@ -148,6 +155,7 @@ class ActivationController : private juce::Timer, std::atomic licensed_{false}; // mirror of license_.has_value() for the audio thread std::optional expiredTrial_; // display-only backing for the Expired screen std::optional pendingRequest_; + std::optional previewClock_; // pinned "now" for previews/snapshots (trialDaysRemaining) juce::String statusMessage_; juce::String offlineError_; diff --git a/tests/visual/snapshot_main.cpp b/tests/visual/snapshot_main.cpp index 6c87651..e50112b 100644 --- a/tests/visual/snapshot_main.cpp +++ b/tests/visual/snapshot_main.cpp @@ -44,6 +44,7 @@ ERUn++6CVMPvZo67jVbTY+GCXYfW4gGVZQIDAQAB -----END RSA PUBLIC KEY-----)"; config.productName = "Solstice"; config.manufacturerName = "Helio Audio"; + config.deviceName = "Studio Mac"; // fixed so the device chip doesn't render the build host's name config.accent = juce::Colour(0xff186cdc); config.trialLengthDays = 14; config.trialFeatures = { @@ -61,6 +62,12 @@ ERUn++6CVMPvZo67jVbTY+GCXYfW4gGVZQIDAQAB return config; } +// A fixed wall-clock anchor for every rendered date and trial countdown, so the +// snapshots are identical run to run regardless of the real date. Paired with +// ActivationController::setPreviewClock so trialDaysRemaining() measures against +// the same instant the license timestamps are built from. 2026-01-01 12:00:00 UTC. +const std::chrono::system_clock::time_point kPreviewNow{ std::chrono::seconds(1767268800) }; + moonbase::license makeLicense(bool trial) { moonbase::license lic; @@ -75,11 +82,10 @@ moonbase::license makeLicense(bool trial) lic.issued_to.name = "Alex Rivera"; lic.issued_to.email = "alex@northward.studio"; - const auto now = std::chrono::system_clock::now(); - lic.issued_at = now; - lic.validated_at = now; + lic.issued_at = kPreviewNow; + lic.validated_at = kPreviewNow; if (trial) - lic.expires_at = now + std::chrono::hours(24 * 11); // 11 of 14 days left + lic.expires_at = kPreviewNow + std::chrono::hours(24 * 11); // 11 of 14 days left lic.seat_count = 3; // 2 of 3 device seats used lic.seats_used = 2; lic.token = "preview.preview.preview"; @@ -98,7 +104,7 @@ moonbase::license makeOfflineLicense() moonbase::license makeExpiredTrial() { auto lic = makeLicense(true); - lic.expires_at = std::chrono::system_clock::now() - std::chrono::hours(24 * 6); // ended 6 days ago + lic.expires_at = kPreviewNow - std::chrono::hours(24 * 6); // ended 6 days ago lic.seat_count.reset(); lic.seats_used.reset(); return lic; @@ -109,7 +115,7 @@ moonbase::license makeExpiredTrial() moonbase::license makeSubscriptionLicense() { auto lic = makeLicense(false); - lic.expires_at = std::chrono::system_clock::now() + std::chrono::hours(24 * 27); // renews in ~27 days + lic.expires_at = kPreviewNow + std::chrono::hours(24 * 27); // renews in ~27 days return lic; } @@ -123,6 +129,7 @@ void writeSnapshot(const juce::File& outDir, const juce::String& name, ActivationComponent component(std::move(config)); component.onClose = [] {}; // enable the close button for the snapshots component.setSize(gSnapW, gSnapH); + component.controller().setPreviewClock(kPreviewNow); // pin the trial countdown // setPreviewState notifies synchronously, so the screen is active immediately. setup(component.controller());