Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5dd88c0
feat: pass legacy license keys to Herald for updates and downloads
d4mation May 15, 2026
aafb592
tweak: drop malformed legacy license entries at repository intake
d4mation May 15, 2026
f0fc769
docs: describe legacy license behavior in availability and downloads
d4mation May 15, 2026
b3ca095
fix(tests): rename it_* methods to test_* in License_RepositoryTest
d4mation May 15, 2026
d1b0a23
docs: explain why URL builder inverts the resolver's Unified/Legacy o…
d4mation May 15, 2026
f1d962a
Add rawurlencodes to cspell
d4mation May 15, 2026
73f2c19
refactor(features): evaluate legacy grant first to simplify availabil…
d4mation May 18, 2026
23ba58b
tweak(legacy): use generic truthiness check for required license fields
d4mation May 18, 2026
901a6e2
refactor(portal): extract herald URL composition into dedicated helpers
d4mation May 18, 2026
86e9ff0
tweak(portal): use truthy domain check to allow future nullability
d4mation May 18, 2026
84974ee
refactor(portal): keep Herald_Url_Builder constructor compatible by s…
d4mation May 18, 2026
21341ac
refactor(features): make Legacy_License_Repository constructor argume…
d4mation May 18, 2026
0a5dc39
docs(tests): add Tests/@return docblocks to new test methods on this …
d4mation May 18, 2026
27b991e
chore(ci): fix spell check and markdown lint failures
d4mation May 18, 2026
cf739c3
feat: gate legacy-key Harbor updates behind use_for_updates opt-in
d4mation May 18, 2026
ef6d83c
docs(tests): add Tests/@return docblocks to any_used_for_updates tests
d4mation May 18, 2026
169d2dd
docs(portal): drop em dashes from URL builder section
d4mation May 18, 2026
f962156
Merge branch 'main' into feature/utilize-registered-legacy-licenses-f…
d4mation May 18, 2026
1d6299f
Build frontend assets
d4mation May 18, 2026
8cacfe2
fix(legacy): guard License_Repository against filter re-entry recursion
d4mation May 19, 2026
015d930
refactor(features): resolve legacy licenses once per feature collection
d4mation May 19, 2026
d11e388
docs(tests): reframe legacy-filter dispatch test as design intent
d4mation May 19, 2026
ae5f2a2
cspell fix
d4mation May 19, 2026
561a0fd
Improve tooltip message for available but not installable updates
d4mation May 19, 2026
ae6161b
Build frontend assets
d4mation May 19, 2026
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
5 changes: 5 additions & 0 deletions build-dev/index-rtl.css
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,11 @@
.lw-harbor-ui .filter {
filter: var(--lw-harbor-tw-blur,) var(--lw-harbor-tw-brightness,) var(--lw-harbor-tw-contrast,) var(--lw-harbor-tw-grayscale,) var(--lw-harbor-tw-hue-rotate,) var(--lw-harbor-tw-invert,) var(--lw-harbor-tw-saturate,) var(--lw-harbor-tw-sepia,) var(--lw-harbor-tw-drop-shadow,) !important;
}
.lw-harbor-ui .transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --lw-harbor-tw-gradient-from, --lw-harbor-tw-gradient-via, --lw-harbor-tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, backdrop-filter, display, content-visibility, overlay, pointer-events !important;
transition-timing-function: var(--lw-harbor-tw-ease, var(--default-transition-timing-function)) !important;
transition-duration: var(--lw-harbor-tw-duration, var(--default-transition-duration)) !important;
}
.lw-harbor-ui .transition-\[border-radius\] {
transition-property: border-radius !important;
transition-timing-function: var(--lw-harbor-tw-ease, var(--default-transition-timing-function)) !important;
Expand Down
2 changes: 1 addition & 1 deletion build-dev/index.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => 'f86224dd411ed4993f5f');
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '485019c0ccf8491204de');
5 changes: 5 additions & 0 deletions build-dev/index.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion build-dev/index.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build-dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1396,7 +1396,7 @@ function FeatureRow({
feature: feature,
pendingAction: pendingAction,
installableBusy: installableBusy,
upgradeLabel: licenseBadgeType === 'legacy' ? (0,_wordpress_i18n__WEBPACK_IMPORTED_MODULE_1__.__)('Upgrade your license to receive updates and support.', '%TEXTDOMAIN%') : undefined,
upgradeLabel: licenseBadgeType === 'legacy' ? (0,_wordpress_i18n__WEBPACK_IMPORTED_MODULE_1__.__)('Upgrade your license to manage updates from the Software License Manager.', '%TEXTDOMAIN%') : undefined,
onUpdate: licenseBadgeType === 'legacy' || licenseBadgeType === 'revoked' ? undefined : handleUpdate
}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_14__.jsx)(_components_atoms_StatusBadge__WEBPACK_IMPORTED_MODULE_7__.StatusBadge, {
status: badgeStatus
Expand Down
2 changes: 1 addition & 1 deletion build-dev/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/index-rtl.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/index.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => 'd50596f2fb801d5f99cc');
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '70cc98f4cfb6c271a1a8');
2 changes: 1 addition & 1 deletion build/index.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 32 additions & 16 deletions docs/guides/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,14 @@ add_filter('lw-harbor/legacy_licenses', function (array $licenses): array {

foreach ($storedLicenses as $license) {
$licenses[] = [
'key' => $license['key'], // The license key string
'slug' => $license['slug'], // The product/add-on slug this key covers
'name' => $license['name'], // Human-readable product name
'product' => 'your-product', // Product brand slug
'is_active' => $license['is_active'], // bool
'page_url' => admin_url('...'), // Where the user can manage this license
'expires_at' => $license['expires'], // Optional: ISO date string e.g. "2026-01-01"
'key' => $license['key'], // The license key string
'slug' => $license['slug'], // The product/add-on slug this key covers
'name' => $license['name'], // Human-readable product name
'product' => 'your-product', // Product brand slug
'is_active' => $license['is_active'], // bool
'use_for_updates' => true, // Opt-in: route updates and downloads via Herald. See below.
'page_url' => admin_url('...'), // Where the user can manage this license
'expires_at' => $license['expires'], // Optional: ISO date string e.g. "2026-01-01"
];
}

Expand All @@ -118,18 +119,33 @@ add_filter('lw-harbor/legacy_licenses', function (array $licenses): array {

**Legacy license array fields:**

| Field | Required | Description |
| ------------ | -------- | ------------------------------------------------- |
| `key` | Yes | The license key string. |
| `slug` | Yes | The product/add-on slug this key applies to. |
| `name` | Yes | Human-readable product name. |
| `product` | Yes | Product brand slug (e.g. `givewp`, `kadence`). |
| `is_active` | Yes | Whether the license is currently active (`bool`). |
| `page_url` | Yes | Admin URL where the user can manage this license. |
| `expires_at` | No | Expiry date string (e.g. `"2026-01-01"`). |
| Field | Required | Description |
| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `key` | Yes | The license key string. |
| `slug` | Yes | The product/add-on slug this key applies to. |
| `name` | Yes | Human-readable product name. |
| `product` | Yes | Product brand slug (e.g. `givewp`, `kadence`). |
| `is_active` | Yes | Whether the license is currently active (`bool`). |
| `use_for_updates` | No | Opt-in (`bool`, default `false`). Set to `true` only when the key is compatible with Stellar Licensing v3 / Herald. |
| `page_url` | Yes | Admin URL where the user can manage this license. |
| `expires_at` | No | Expiry date string (e.g. `"2026-01-01"`). |

> **Tip:** If a single license key covers multiple add-ons, emit one entry per add-on slug so each slug can display a legacy license badge on the Feature Manager page.

### How Harbor uses reported legacy keys

Reported entries always appear in the unified license UI and feed admin notices. Whether they also feed Harbor's update pipeline (feature availability and download URLs) depends on the `use_for_updates` opt-in:

1. **Opt-in entries (`use_for_updates = true`).** When such an entry is `is_active = true` with a non-empty `key`, it marks the catalog feature matching its `slug` as available and in-tier (even with no unified license installed, or when the installed unified tier does not include that feature). Update checks proceed for that slug, and the package URL routes through Herald's `/legacy/download` endpoint using the reported key.
2. **Opt-out or omitted (`use_for_updates = false`, the default).** The entry is informational only. It appears in the licensing UI and admin notices, but does not grant availability, does not show "update available" badges, and is never sent to Herald.
3. **Inactive entries (`is_active = false`).** Same informational-only treatment, plus the entry surfaces in admin notices urging the user to renew or reactivate.

**When to set `use_for_updates = true`.** Only when the legacy key is compatible with Stellar Licensing v3 (the system Herald authenticates against on `/legacy/download`). Plugins whose legacy keys are issued by a separate licensing backend (for example, SolidWP API keys, or some Give legacy keys) should leave it `false`: surfacing an "update available" badge for a key Herald cannot validate would lead to download failures during install. When in doubt, leave it off and the entry continues to display in the UI/notices without breaking anything.

**What `is_active` means.** Harbor takes this flag at face value from your plugin. It should reflect whatever your existing licensing system already considers a valid, in-good-standing license: for example, the result of a recent successful validation against your licensing server. Harbor does not (and cannot) independently verify the key; it trusts the reporting plugin to decide whether the customer is currently entitled to use the product. Regardless of the `is_active` value reported here, Herald validates the key server-side when serving the actual ZIP download, so a falsely-reported `is_active = true` cannot be used to obtain a package the customer is not entitled to.

**Malformed entries.** `key` and `slug` are both required (see the table above). Entries missing either field are not considered legacy licenses at all. They are dropped at repository intake and never appear in the UI, notices, availability checks, or download URLs. Only emit entries you have a real key for.
Comment thread
d4mation marked this conversation as resolved.

### Admin notices for inactive legacy licenses

Once you report licenses via this filter, Harbor automatically displays consolidated admin notices for any inactive licenses that are not already covered by a StellarWP v3 unified license. Notices are grouped by product, shown only to administrators, and are dismissible per user for 7 days.
Expand Down
10 changes: 7 additions & 3 deletions docs/subsystems/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,18 @@ flowchart TD
HasLicense -->|No| FreeTier{"min_tier at\nrank 0 (free)?"}

InCaps -->|Yes| Available["Available"]
InCaps -->|No| Unavailable["Unavailable"]
InCaps -->|No| LegacyCheck{"Active legacy license\nfor this slug?"}

FreeTier -->|Yes| AvailableFallback["Available\n(fallback)"]
FreeTier -->|No| UnavailableFallback["Unavailable"]
FreeTier -->|No| LegacyCheck

LegacyCheck -->|Yes| AvailableLegacy["Available\n(legacy grant)"]
LegacyCheck -->|No| Unavailable["Unavailable"]

Available --> EnabledCheck
Unavailable --> EnabledCheck
AvailableFallback --> EnabledCheck
UnavailableFallback --> EnabledCheck
AvailableLegacy --> EnabledCheck

EnabledCheck{"Check local enabled state\n(per strategy)"}

Expand All @@ -99,6 +102,7 @@ Edge cases:
- No licensing entry for a product (unlicensed): the resolver falls back to tier rank comparison using rank 0, making only free-tier features (`minimum_tier` at rank 0) available. Paid-tier features are unavailable.
- A feature capable but outside the catalog tier: it is available — capabilities override the catalog tier.
- A feature in the customer's catalog tier but absent from capabilities: it is unavailable — capabilities are the authority.
- A legacy license whose `slug` matches a catalog feature can grant availability (and in-tier status) even with no Unified license, or with a Unified tier that doesn't include the feature. All four conditions must hold for the grant to apply: the entry's `key` is non-empty, `is_active` is `true`, and the reporting plugin has opted in with `use_for_updates = true`. The opt-in protects Harbor from advertising updates for legacy keys whose backend is not Stellar Licensing v3 compatible (e.g. SolidWP API keys), which would otherwise fail validation at Herald during download. Resolution checks Unified entitlement first and treats legacy as a fallback grant; download URL construction uses the inverse order so the legacy key authenticates downloads for its specific slug. See [Portal: Download URL Builder](portal.md#download-url-builder) for the rationale and the resulting URL format.

## The Manager

Expand Down
Loading