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
5 changes: 5 additions & 0 deletions changelog/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
significance: patch
type: feature
entry: Added the `lw_harbor_refresh_catalog()` global function to force a
synchronous re-fetch of the product catalog from the Commerce Portal API.
timestamp: 2026-05-21T14:27:00.315Z
29 changes: 19 additions & 10 deletions docs/guides/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,16 @@ 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`). |
| `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"`). |
| 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.

Expand Down Expand Up @@ -228,6 +228,14 @@ if (lw_harbor_is_feature_available('feature-slug')) {
$url = lw_harbor_get_license_page_url(); // string (empty string if Harbor is not active)
```

### Force a catalog refresh

```php
$refreshed = lw_harbor_refresh_catalog(); // bool — true on success, false on failure
```

Bypasses Harbor's cached catalog and fetches a fresh copy from the Commerce Portal API. This is **synchronous** — the call blocks until the upstream responds — so reserve it for user-initiated actions (e.g. a "Refresh now" button) rather than passive page loads. Returns `false` when Harbor is not active or the upstream fetch fails; the previously cached catalog is preserved in that case so subsequent reads continue to work.

---

## 5. Registering a Submenu Link
Expand Down Expand Up @@ -291,3 +299,4 @@ See [Section 2](#2-bundling-a-license-key). Bundling a key is done entirely thro
| `lw_harbor_get_licensed_domain` | `(): string` | Get the domain Harbor uses for licensing on this site. |
| `lw_harbor_register_submenu` | `(string $parent_slug): void` | Append a Licensing submenu item to a plugin's top-level admin menu. No-op until `lw_harbor/loaded` has fired. |
| `lw_harbor_display_legacy_license_page_notice` | `(string $product_name = ''): void` | Display a notice on a legacy license page pointing users to the unified system. |
| `lw_harbor_refresh_catalog` | `(): bool` | Force a synchronous re-fetch of the product catalog. Returns `true` on success, `false` on failure. |
23 changes: 23 additions & 0 deletions src/Harbor/API/Functions/Global_Function_Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use LiquidWeb\Harbor\Config;
use LiquidWeb\Harbor\Features\Manager;
use LiquidWeb\Harbor\Licensing\Repositories\License_Repository;
use LiquidWeb\Harbor\Portal\Catalog_Repository;
use LiquidWeb\Harbor\Site\Data;
use LiquidWeb\Harbor\Traits\With_Debugging;
use Throwable;
Expand Down Expand Up @@ -155,5 +156,27 @@ static function (): string {
}
}
);

\_lw_harbor_global_function_registry(
'lw_harbor_refresh_catalog',
$version,
static function (): bool {
try {
$result = Config::get_container()->get( Catalog_Repository::class )->refresh();

if ( is_wp_error( $result ) ) {
self::debug_log_wp_error( $result, 'Error refreshing catalog' );

return false;
}

return true;
} catch ( Throwable $e ) {
self::debug_log_throwable( $e, 'Error refreshing catalog' );

return false;
}
}
);
}
}
1 change: 1 addition & 0 deletions src/Harbor/API/Functions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Strauss rewrites class references at parse time. `License_Repository::class` ins
| `lw_harbor_register_submenu( $parent_slug )` | Appends a Licensing submenu item under a plugin's top-level admin menu |
| `lw_harbor_display_legacy_license_page_notice( $product_name = '' )` | Renders an info notice on a plugin's legacy license page pointing users to the unified system |
| `lw_harbor_has_consent()` | Whether the site owner has opted in to external Liquid Web API communications |
| `lw_harbor_refresh_catalog()` | Force a synchronous re-fetch of the product catalog from the Commerce Portal API |

Each function looks up the registered callback and delegates, returning `false` if no callback is registered yet:

Expand Down
23 changes: 23 additions & 0 deletions src/Harbor/global-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,29 @@ function lw_harbor_get_license_page_url(): string {
}
}

if ( ! function_exists( 'lw_harbor_refresh_catalog' ) ) {
/**
* Forces a synchronous fresh catalog fetch from the Commerce Portal API.
*
* Bypasses the cached catalog state and re-populates it from the upstream
* service. Use after any action where the caller has reason to believe the
* remote catalog has changed (e.g. a user-initiated "refresh" button).
*
* Synchronous: this performs an outbound HTTP request and blocks until the
* Portal API responds. Returns true on a successful refresh, false on any
* failure (HTTP error, parsing error, or Harbor not being active).
*
* @since TBD
*
* @return bool Whether the catalog was refreshed successfully.
*/
function lw_harbor_refresh_catalog(): bool {
$callback = _lw_harbor_global_function_registry( 'lw_harbor_refresh_catalog' );

return $callback ? (bool) $callback() : false;
}
}

if ( ! function_exists( 'lw_harbor_display_legacy_license_page_notice' ) ) {
/**
* Displays an informational admin notice on legacy plugin license pages.
Expand Down
44 changes: 44 additions & 0 deletions tests/wpunit/API/Functions/GlobalFunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
use LiquidWeb\Harbor\Licensing\Repositories\License_Repository;
use LiquidWeb\Harbor\Licensing\Product_Collection;
use LiquidWeb\Harbor\Licensing\Results\Product_Entry;
use LiquidWeb\Harbor\Portal\Catalog_Collection;
use LiquidWeb\Harbor\Portal\Catalog_Repository;
use LiquidWeb\Harbor\Tests\HarborTestCase;
use LiquidWeb\Harbor\Harbor;
use WP_Error;

/**
* Tests for the global helper functions defined in global-functions.php.
Expand Down Expand Up @@ -232,4 +235,45 @@ public function test_get_licensed_domain_matches_site_url_host(): void {
$this->assertNotEmpty( $domain );
$this->assertSame( $expected, $domain );
}

// -------------------------------------------------------------------------
// lw_harbor_refresh_catalog()
// -------------------------------------------------------------------------

public function test_refresh_catalog_invokes_repository_refresh_and_returns_true_on_success(): void {
$called = false;

$catalog = $this->makeEmpty(
Catalog_Repository::class,
[
'refresh' => static function () use ( &$called ): Catalog_Collection {
$called = true;

return new Catalog_Collection();
},
]
);

$this->container->singleton( Catalog_Repository::class, $catalog );

$result = lw_harbor_refresh_catalog();

$this->assertTrue( $called );
$this->assertTrue( $result );
}

public function test_refresh_catalog_returns_false_when_refresh_returns_wp_error(): void {
$catalog = $this->makeEmpty(
Catalog_Repository::class,
[
'refresh' => static function (): WP_Error {
return new WP_Error( 'catalog_error', 'API unavailable.' );
},
]
);

$this->container->singleton( Catalog_Repository::class, $catalog );

$this->assertFalse( lw_harbor_refresh_catalog() );
}
}
Loading