Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 93 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -212,20 +213,102 @@ native provider and `<moonbase/http_curl.hpp>` 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.

<p align="center">
<img src="assets/moonbase-juce-license.png" width="600"
alt="The moonbase_licensing activation UI showing license details: licensed-to name, email, plan, activation type, expiry, seat count, and a Deactivate this device button.">
</p>

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`.
Expand All @@ -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:
Expand Down
Binary file added assets/moonbase-juce-license.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/moonbase-juce-trial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions modules/moonbase_licensing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<p align="center">
<img src="../../assets/moonbase-juce-trial.png" width="49%"
alt="Trial screen: a free-trial panel with days remaining, a progress bar, and an Unlock full version button.">
<img src="../../assets/moonbase-juce-license.png" width="49%"
alt="License details screen: licensed-to name, email, plan, activation type, expiry, seat count, and a Deactivate this device button.">
</p>

## Features

- **Native integration.** Talks to the Moonbase licensing API directly: online
Expand Down
5 changes: 5 additions & 0 deletions modules/moonbase_licensing/juce/ActivationConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
// "<name> · <platform>" 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:
Expand Down
12 changes: 10 additions & 2 deletions modules/moonbase_licensing/juce/ActivationController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -684,14 +686,20 @@ void ActivationController::setPreviewState(Screen screen, std::optional<moonbase
sendSynchronousChangeMessage();
}

void ActivationController::setPreviewClock(std::optional<std::chrono::system_clock::time_point> 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<std::chrono::seconds>(
*license_->expires_at - std::chrono::system_clock::now())
*license_->expires_at - now)
.count();
if (seconds <= 0)
return 0;
Expand Down
8 changes: 8 additions & 0 deletions modules/moonbase_licensing/juce/ActivationController.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// license() and repaint.

#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <memory>
Expand Down Expand Up @@ -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<std::chrono::system_clock::time_point> now);

//== Accessors =============================================================
[[nodiscard]] Screen screen() const noexcept { return screen_; }
[[nodiscard]] const std::optional<moonbase::license>& license() const noexcept { return license_; }
Expand Down Expand Up @@ -148,6 +155,7 @@ class ActivationController : private juce::Timer,
std::atomic<bool> licensed_{false}; // mirror of license_.has_value() for the audio thread
std::optional<moonbase::license> expiredTrial_; // display-only backing for the Expired screen
std::optional<moonbase::activation_request> pendingRequest_;
std::optional<std::chrono::system_clock::time_point> previewClock_; // pinned "now" for previews/snapshots (trialDaysRemaining)

juce::String statusMessage_;
juce::String offlineError_;
Expand Down
19 changes: 13 additions & 6 deletions tests/visual/snapshot_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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;
Expand All @@ -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";
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -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());
Expand Down
Loading