-
Notifications
You must be signed in to change notification settings - Fork 0
Add a reusable activation URL API #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dave-green-uk
wants to merge
33
commits into
bucket/activation-flow-api
from
smtnc-1844-harbor-expose-a-reusable-activation-url-api-for-php-and-js
Open
Changes from 29 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 af49f7d
Build frontend assets
dave-green-uk d97364c
Fix the default activation redirect landing on a WordPress error page
dave-green-uk 7de8400
Correct the enqueue timing guidance for the activation script
dave-green-uk d3fc2b1
Correct the rationale for the canonical redirect URL
dave-green-uk bd1a42a
Use US spelling in the activation URL test docblock
dave-green-uk f4b36ce
Refresh licensing data when the portal returns a user to the site
dave-green-uk d78dad3
Fix fallout from moving the refresh out of the admin page
dave-green-uk 1c8efec
Realign the constructor docblock after dropping a parameter
dave-green-uk 0a4a279
Check the return tag before resolving the refresh handler
dave-green-uk dc9f24f
Expose activation URLs through stable global functions
dave-green-uk 006b85b
Align the activation-URL doc tables to satisfy markdownlint
dave-green-uk 081c461
Address review feedback on the activation URL API
dave-green-uk 189b9c1
Stop suppressing exit() in the test suite
dave-green-uk cf6ed65
Fix the redirect stand-in's class scope and a docblock tag
dave-green-uk a5f52f1
Warn at runtime when the deprecated redirect handler is called
dave-green-uk 7c0651b
Merge bucket/activation-flow-api into the activation URL API branch
dave-green-uk bc40333
Address the second review round on the activation URL API
dave-green-uk e203f94
Build frontend assets
dave-green-uk 38455b8
Isolate the enqueue hook in the script dependency tests
dave-green-uk f981248
Return null rather than an empty string when there is no activation URL
dave-green-uk 6ebaeef
Expose product license lookups so consumers need not touch internal c…
dave-green-uk e58fb2f
Drop the browser activation helper in favour of building URLs in PHP
dave-green-uk 11d89ed
Let the return handler decide whether a request is a return trip
dave-green-uk ae32efd
Cover the compiled asset resolver with unit tests
dave-green-uk 4067013
Drop the changelog note for an argument that never shipped
dave-green-uk 2c50995
Qualify the base activation URL function as product activation
dave-green-uk f8df048
Fix two spellings cspell rejects
dave-green-uk 4735e06
Inject the catalog repository rather than service-locating it
dave-green-uk a52253b
remove LiquidWeb\Harbor\Utils\Assets class
pramodjodhani d6ebc7b
Merge branch 'smtnc-1844-harbor-expose-a-reusable-activation-url-api-…
pramodjodhani 6375aac
remove AssetsTest.php
pramodjodhani 96ade84
repalce maybe_refresh_after_activation with closure/anon funciton
pramodjodhani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| significance: minor | ||
| type: feature | ||
| entry: Licensing data is now refreshed automatically when the Liquid Web portal returns a user to the site after activating, so screens gated on license state are correct on arrival. Host plugins need no code for this; sending the user through an activation URL is the whole opt-in. This supersedes the Software Manager page's ?refresh=auto handler, which only covered that one screen; Feature_Manager_Page::maybe_redirect_after_refresh() is deprecated and no longer hooked | ||
| timestamp: 2026-07-22T00:00:00.000Z |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 via the lw_harbor_get_product_activation_base_url() and lw_harbor_get_product_activation_url() global functions, and pass them to their own onboarding screens | ||
| timestamp: 2026-07-22T00:00:00.000Z |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| significance: minor | ||
| type: feature | ||
| entry: Added lw_harbor_is_product_licensed() and lw_harbor_get_product_tier() so host plugins can ask whether a license covers a product, and at which tier, without resolving Harbor's internal licensing classes from their own vendor-prefixed copy | ||
| timestamp: 2026-07-30T00:00:00.000Z |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| # 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 | ||
| # the :{tier} half is omitted when unknown | ||
| ``` | ||
|
|
||
| `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. Without a | ||
| tier the portal offers a picker limited to the activating domain, so a partial | ||
| `sku` narrows the choice rather than failing. | ||
|
|
||
| Harbor appends `lw-harbor-activated=1` to whatever return URL you supply. That | ||
| tag lives inside `redirect_url`, not at the top level — see below. | ||
|
|
||
| ## The return trip refreshes your data automatically | ||
|
|
||
| Licensing data is cached. Without a refresh, a user who has just activated in | ||
| the portal comes back to a screen that still believes they are unlicensed: the | ||
| Activate button is still there, the feature they paid for is still gated. | ||
|
|
||
| You do not have to handle this. Harbor tags every return URL it builds and | ||
| watches for that tag on any admin screen. On the way back it refreshes the | ||
| license products and the catalog, strips the tag, and redirects — all on | ||
| `admin_init`, before your page renders. By the time your code runs, | ||
| `License_Repository` is current. | ||
|
|
||
| Consequences worth knowing: | ||
|
|
||
| - **Read licensing state at render time**, not from something cached earlier in | ||
| the request. The refresh has already happened by then. | ||
| - **The URL the user lands on is not the one you supplied** — it briefly carries | ||
| `lw-harbor-activated=1`, then redirects to your clean URL. Anything that | ||
| fingerprints the query string should tolerate that. | ||
| - **Only one instance refreshes.** The handler is behind the same version | ||
| leadership check as the rest of Harbor, so four active plugins using Harbor make | ||
| one API call between them, not four. | ||
|
d4mation marked this conversation as resolved.
|
||
| - **It requires `manage_options`.** The tag rides on a URL your plugin owns, so | ||
| it can land on a screen with no capability check of its own. | ||
| - **Failures are logged, not surfaced.** If the refresh fails the user still | ||
| reaches your page, with stale data. They have just come back from activating | ||
| and are looking at your screen, not a licensing one, so an error notice there | ||
| would be noise they cannot act on. | ||
|
|
||
| ## From PHP | ||
|
|
||
| Call the global functions. Like the rest of Harbor's public API they resolve to | ||
| the highest-version Harbor copy on the site, so you always get the loaded | ||
| version's logic — do not build the URL from a Harbor class in your own bundled | ||
| copy, which may not be the one actually running. | ||
|
|
||
| ```php | ||
| // Product-scoped, returning the user to your onboarding screen. | ||
| $href = lw_harbor_get_product_activation_url( | ||
| 'kadence', | ||
| lw_harbor_get_product_tier( 'kadence' ), | ||
| add_query_arg( | ||
| [ | ||
| 'page' => 'kadence-onboarding', | ||
| 'step' => 2, | ||
| ], | ||
| admin_url( 'admin.php' ) | ||
| ) | ||
| ); | ||
| ``` | ||
|
|
||
| | Function | Returns | | ||
| | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | ||
| | `lw_harbor_get_product_activation_base_url( ?string $redirect_url )` | The portal subscriptions URL with referral, redirect, and domain params | | ||
| | `lw_harbor_get_product_activation_url( string $slug, ?string $tier, ?string $redirect_url )` | The same, plus `sku={slug}` and `:{tier}` when a tier is given | | ||
| | `lw_harbor_is_product_licensed( string $slug )` | Whether the stored license covers the product at all, activated or not | | ||
| | `lw_harbor_get_product_tier( string $slug )` | The licensed tier, or `null` when absent or licensed at several | | ||
|
|
||
| The URL builders return `null` when no Harbor instance is active, or when the URL | ||
| could not be built — treat that as "hide the button". 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. | ||
|
|
||
| ```php | ||
| $href = lw_harbor_get_product_activation_base_url( $return_url ); | ||
|
|
||
| if ( null === $href ) { | ||
| return; // Nothing to offer. | ||
| } | ||
| ``` | ||
|
|
||
| ### Do not look the tier up yourself | ||
|
|
||
| `$tier` is optional, and `lw_harbor_get_product_tier()` is the supported way to | ||
| find one. Pass its result straight through, including when it is `null`: an | ||
| unscoped `sku` sends the user to the portal's product and tier picker, still | ||
| scoped to the activating domain, which is the right screen when the license | ||
| covers the product at more than one tier. | ||
|
|
||
| Reaching into `License_Repository` or `Product_Entry` from your own bundled copy | ||
| to read a tier is the thing this API exists to replace. Those classes are | ||
| Strauss-prefixed per plugin, and only the highest-version copy refreshes the | ||
| catalog — so you would be reading the leader's data with your own, possibly | ||
| older, code. | ||
|
|
||
| ```php | ||
| // Licensed but not yet activated here: the state worth prompting on. | ||
| if ( | ||
| lw_harbor_is_product_licensed( 'kadence' ) | ||
| && ! lw_harbor_is_product_license_active( 'kadence' ) | ||
| ) { | ||
| $href = lw_harbor_get_product_activation_url( | ||
| 'kadence', | ||
| lw_harbor_get_product_tier( 'kadence' ), | ||
| $return_url | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ### Getting the return URL right | ||
|
|
||
| Prefer your page's canonical address — the parent it is actually registered | ||
| under: | ||
|
|
||
| | How your page is registered | Canonical 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 will resolve an `admin.php?page={slug}` URL to a submenu page anyway, | ||
| so this is a consistency preference rather than a correctness requirement. | ||
| `menu_page_url( 'your-slug', false )` returns the canonical form without | ||
| hardcoding the parent. | ||
|
|
||
| The examples below assume a top-level menu. | ||
|
|
||
| ## From JavaScript | ||
|
|
||
| Harbor does not ship a browser API for this. Build the URL in PHP and hand it to | ||
| your script, so the `sku` contract lives in exactly one place. | ||
|
|
||
| ```php | ||
| wp_localize_script( | ||
| 'kadence-onboarding', | ||
| 'kadenceOnboarding', | ||
| [ | ||
| 'activationUrl' => lw_harbor_get_product_activation_url( | ||
| 'kadence', | ||
| lw_harbor_get_product_tier( 'kadence' ), | ||
| menu_page_url( 'kadence-onboarding', false ) | ||
| ), | ||
| ] | ||
| ); | ||
| ``` | ||
|
|
||
| ```js | ||
| if ( kadenceOnboarding.activationUrl ) { | ||
| // safe to link to | ||
| } | ||
| ``` | ||
|
|
||
| The function returns `null` when no Harbor instance is active, so a falsy value | ||
| is your signal to hide the control rather than render a dead link. | ||
|
|
||
| When the tier is chosen in the browser, localize one URL per tier and pick | ||
| between them client-side: | ||
|
|
||
| ```php | ||
| $tiers = []; | ||
|
|
||
| foreach ( [ 'plus', 'pro' ] as $tier ) { | ||
| $tiers[ $tier ] = lw_harbor_get_product_activation_url( 'kadence', $tier, $return_url ); | ||
| } | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.