Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5af561a
Add a reusable activation URL API for PHP and JS
dave-green-uk Jul 22, 2026
af49f7d
Build frontend assets
dave-green-uk Jul 22, 2026
d97364c
Fix the default activation redirect landing on a WordPress error page
dave-green-uk Jul 22, 2026
7de8400
Correct the enqueue timing guidance for the activation script
dave-green-uk Jul 22, 2026
d3fc2b1
Correct the rationale for the canonical redirect URL
dave-green-uk Jul 22, 2026
bd1a42a
Use US spelling in the activation URL test docblock
dave-green-uk Jul 22, 2026
f4b36ce
Refresh licensing data when the portal returns a user to the site
dave-green-uk Jul 22, 2026
d78dad3
Fix fallout from moving the refresh out of the admin page
dave-green-uk Jul 22, 2026
1c8efec
Realign the constructor docblock after dropping a parameter
dave-green-uk Jul 22, 2026
0a4a279
Check the return tag before resolving the refresh handler
dave-green-uk Jul 22, 2026
dc9f24f
Expose activation URLs through stable global functions
dave-green-uk Jul 24, 2026
006b85b
Align the activation-URL doc tables to satisfy markdownlint
dave-green-uk Jul 24, 2026
081c461
Address review feedback on the activation URL API
dave-green-uk Jul 27, 2026
189b9c1
Stop suppressing exit() in the test suite
dave-green-uk Jul 27, 2026
cf6ed65
Fix the redirect stand-in's class scope and a docblock tag
dave-green-uk Jul 27, 2026
a5f52f1
Warn at runtime when the deprecated redirect handler is called
dave-green-uk Jul 27, 2026
7c0651b
Merge bucket/activation-flow-api into the activation URL API branch
dave-green-uk Jul 28, 2026
bc40333
Address the second review round on the activation URL API
dave-green-uk Jul 28, 2026
e203f94
Build frontend assets
dave-green-uk Jul 28, 2026
38455b8
Isolate the enqueue hook in the script dependency tests
dave-green-uk Jul 28, 2026
f981248
Return null rather than an empty string when there is no activation URL
dave-green-uk Jul 28, 2026
6ebaeef
Expose product license lookups so consumers need not touch internal c…
dave-green-uk Jul 30, 2026
e58fb2f
Drop the browser activation helper in favour of building URLs in PHP
dave-green-uk Aug 12, 2026
11d89ed
Let the return handler decide whether a request is a return trip
dave-green-uk Aug 12, 2026
ae32efd
Cover the compiled asset resolver with unit tests
dave-green-uk Aug 12, 2026
4067013
Drop the changelog note for an argument that never shipped
dave-green-uk Aug 12, 2026
2c50995
Qualify the base activation URL function as product activation
dave-green-uk Aug 12, 2026
f8df048
Fix two spellings cspell rejects
dave-green-uk Aug 12, 2026
4735e06
Inject the catalog repository rather than service-locating it
dave-green-uk Aug 12, 2026
a52253b
remove LiquidWeb\Harbor\Utils\Assets class
pramodjodhani Aug 14, 2026
d6ebc7b
Merge branch 'smtnc-1844-harbor-expose-a-reusable-activation-url-api-…
pramodjodhani Aug 14, 2026
6375aac
remove AssetsTest.php
pramodjodhani Aug 14, 2026
96ade84
repalce maybe_refresh_after_activation with closure/anon funciton
pramodjodhani Aug 14, 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
1 change: 1 addition & 0 deletions build-dev/activation.asset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => 'ab69f34667398c80fadf');
148 changes: 148 additions & 0 deletions build-dev/activation.js

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

1 change: 1 addition & 0 deletions build-dev/activation.js.map

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.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' => '15d9bcfeaaf63bae053a');
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '3788ff0adcebe4512b9e');
2 changes: 1 addition & 1 deletion build-dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5740,7 +5740,7 @@ const expiryTextClass = {
* Keys with two or fewer dash-delimited segments have nothing safe to mask
* (no middle to hide), so they are returned unchanged.
*
* @since TBD
* @since 1.5.0
Comment thread
d4mation marked this conversation as resolved.
*/
function maskLicenseKey(key) {
const segments = key.split('-');
Expand Down
2 changes: 1 addition & 1 deletion build-dev/index.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build/activation.asset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => '9221f4ca9e52af97fee3');
1 change: 1 addition & 0 deletions build/activation.js

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

4 changes: 4 additions & 0 deletions changelog/smtnc-1844-activation-url-api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
significance: minor
type: feature
entry: Added a reusable activation URL API. Host plugins can build Liquid Web portal activation URLs from PHP via the Activation_Url service, or in the browser via the shared lw-harbor-activation script, which exposes window.lwHarbor.buildActivationUrl()
timestamp: 2026-07-22T00:00:00.000Z
152 changes: 152 additions & 0 deletions docs/guides/activation-urls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Activation URLs

An activation URL sends the user to the Liquid Web portal with enough context to
activate a product against the current site. Harbor builds these URLs so host
plugins do not each reimplement the portal's query string.

Use this when you need an "Activate" button outside Harbor's own Software
Manager page — for example on a plugin's onboarding screen.

## What the URL contains

```text
{portal_base_url}/subscriptions/
?portal-referral=plugin
&redirect_url={where the portal returns the user}
&domain={this site's domain}
&sku={product_slug}:{tier} # only on product-scoped URLs
```

`redirect_url` is percent-encoded, so its own query string does not leak into
the portal URL as separate params. `sku` is what lets the portal pre-select a
product and tier instead of dropping the user on an unfiltered list.

## From PHP

Resolve `Activation_Url` from the container.

```php
use LiquidWeb\Harbor\Portal\Activation_Url;

$activation_url = Config::get_container()->get( Activation_Url::class );

// Product-scoped, returning the user to your onboarding screen.
$href = $activation_url->for_product(
'kadence',
'pro',
admin_url( 'admin.php?page=kadence-onboarding&step=2' )
Comment thread
d4mation marked this conversation as resolved.
Outdated
);
```

| Method | Returns |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| `get_base( ?string $redirect_url )` | The portal subscriptions URL with referral, redirect, and domain params |
| `for_product( string $slug, string $tier, ?string $redirect_url )` | The same, plus `sku={slug}:{tier}` |

Omit `$redirect_url` to fall back to Harbor's Software Manager page. Pass your
own whenever the user started somewhere else — otherwise they will not come
back to where they were.

### Getting the return URL right

Build the return URL from the parent your page is actually registered under,
not from `admin.php`:

| How your page is registered | Return URL |
| ---------------------------------------------- | --------------------------------- |
| `add_menu_page()` (top level) | `admin.php?page={slug}` |
| `add_submenu_page( 'options-general.php', … )` | `options-general.php?page={slug}` |
| `add_submenu_page( 'tools.php', … )` | `tools.php?page={slug}` |

WordPress resolves a page by a hook name derived from its parent. Address a
Settings submenu through `admin.php` and the lookup misses, so the user lands on
a "Cannot load {slug}" error instead of your onboarding screen — after they have
already paid and activated. Use `menu_page_url( 'your-slug', false )` if you
would rather not hardcode the parent at all.

The examples below assume a top-level menu.

## From JavaScript

Use this when the product or tier is chosen in the browser. If it is fixed at
render time, build the URL in PHP instead and skip the script entirely.

Harbor registers a dependency-free script exposing `window.lwHarbor`. Declare it
as a dependency:

```php
use LiquidWeb\Harbor\Config;
use LiquidWeb\Harbor\Portal\Activation_Script;
use LiquidWeb\Harbor\Portal\Activation_Url;

$activation_url = Config::get_container()->get( Activation_Url::class );

wp_enqueue_script(
'kadence-onboarding',
$url . 'build/onboarding.js',
[ Activation_Script::HANDLE ],
Comment thread
d4mation marked this conversation as resolved.
Outdated
$version,
true
);

// The helper only appends sku, so pass it a base URL built in PHP.
wp_localize_script(
'kadence-onboarding',
'kadenceOnboarding',
[
'activationBaseUrl' => $activation_url->get_base(
admin_url( 'admin.php?page=kadence-onboarding&step=2' )
),
]
);
```

Then in the browser:

```js
const href = window.lwHarbor.buildActivationUrl(
kadenceOnboarding.activationBaseUrl,
'kadence',
selectedTier
);
```

This works from a bundled module or an inline `<script>` — no build step
required on the consuming side.

### Always feature-detect

Every active Harbor copy runs the registration code, but only the highest
version claims it. The API available at runtime is therefore the leader's, which
may be older than the copy your plugin ships.
Comment thread
d4mation marked this conversation as resolved.
Outdated

```js
if ( window.lwHarbor?.buildActivationUrl ) {
// safe to use
}
```

`window.lwHarbor.version` reports the version that actually registered the
script.

### If your script does not load

WordPress silently refuses to print a script whose dependency is not
registered — no error, no console warning. If your onboarding JS goes missing,
check that the handle exists:

```php
wp_script_is( Activation_Script::HANDLE, 'registered' );
```

Harbor registers on `admin_enqueue_scripts` at priority `0`, so enqueuing at the
default priority is safe. Enqueue earlier than that and you will lose the race.

The script is admin-only. It is not registered on the front end.

## Why the handle is not vendor-prefixed

`lw-harbor-activation` and `lwHarbor` are plain strings. Strauss rewrites class
names, not strings, so every Harbor copy on the site agrees on them — which is
what allows a single registration to serve every plugin. This is deliberate; do
not prefix them.
22 changes: 22 additions & 0 deletions resources/js/activation-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Entry point for the shared activation helper script.
*
* Compiled to `build/activation.js` and exposed as `window.lwHarbor` so host
* plugins can build activation URLs in the browser without bundling their own
* copy. Registered as the `lw-harbor-activation` script handle.
*
* Only one Harbor instance registers the script, and it is whichever active
* copy has the highest version. Consumers must therefore feature-detect
* rather than assume a given API is present:
*
* if ( window.lwHarbor?.buildActivationUrl ) { ... }
*
* `window.lwHarbor.version` is appended by PHP after this bundle loads, so it
* always reports the version that actually registered the script.
*
* Keep this entry dependency-free. It loads on admin pages that have nothing
* to do with Harbor's own UI, so it must not pull in React or the store.
*
* @package LiquidWeb\Harbor
*/
export { buildActivationUrl } from '@/lib/activation-url';
Loading
Loading