-
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
base: bucket/activation-flow-api
Are you sure you want to change the base?
Changes from 3 commits
5af561a
af49f7d
d97364c
7de8400
d3fc2b1
bd1a42a
f4b36ce
d78dad3
1c8efec
0a4a279
dc9f24f
006b85b
081c461
189b9c1
cf6ed65
a5f52f1
7c0651b
bc40333
e203f94
38455b8
f981248
6ebaeef
e58fb2f
11d89ed
ae32efd
4067013
2c50995
f8df048
4735e06
a52253b
d6ebc7b
6375aac
96ade84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| <?php return array('dependencies' => array(), 'version' => 'ab69f34667398c80fadf'); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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'); |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| <?php return array('dependencies' => array(), 'version' => '9221f4ca9e52af97fee3'); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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 |
| 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' ) | ||
|
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 ], | ||
|
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. | ||
|
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. | ||
| 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'; |
Uh oh!
There was an error while loading. Please reload this page.