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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

AI Launchpad: address first-pass design feedback (DSGCOM-678) — a single-open accordion task list that auto-expands the next task on skip or completion, WPDS state icons, a Site Editor / Customizer quick link on the site preview, full-width goal copy on mobile, more general social-task subtitles, and removal of the redundant write-3-posts task.
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;

// helpers.php defines the shared option reader the listeners depend on, so it
// loads first. The REST controller and completion listeners self-register on
// their own hooks (rest_api_init / init) at load time, and must run on every
// request regardless of which admin page is showing.
// helpers.php defines the shared option reader the listeners depend on, so it loads first.
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/eligibility.php';
require_once __DIR__ . '/class-ai-launchpad-memberships.php';
Expand All @@ -31,10 +28,7 @@
class AI_Launchpad {

/**
* Admin page slug. The `-wp-admin` suffix is the wp-build convention for
* pages that integrate with the standard wp-admin chrome: the generated
* page PHP only enqueues its assets when `$_GET['page']` matches
* `<page-id>-wp-admin`.
* Admin page slug. The `-wp-admin` suffix is the wp-build convention for pages that integrate with wp-admin chrome.
*/
const MENU_SLUG = 'ai-launchpad-wp-admin';

Expand Down Expand Up @@ -71,23 +65,15 @@ private static function is_ai_launchpad_request() {
/**
* Whether the current site is eligible for the AI Launchpad.
*
* MVP gate: paid plan, not already AI-onboarded, and explicitly enabled for
* the site via the `wpcom_ai_launchpad_enabled` option (set per-site over
* wp-cli). Replaces the earlier automattician/blog-sticker check, which did
* not work on Atomic: `is_automattician()` is undefined there and blog
* stickers require the wpcom sandbox. The option works identically on Simple
* and Atomic and is context-independent (admin, REST, and CLI agree).
* Gate: paid plan, not already AI-onboarded, and explicitly enabled for the site via the `wpcom_ai_launchpad_enabled` option.
*
* @return bool
*/
public static function is_eligible() {
static $eligible = null;

if ( null === $eligible ) {
// Cheapest gate first: the per-site option disqualifies the vast
// majority of sites with a single option read, before the more
// expensive purchases lookup in has_paid_plan() runs on every admin
// page. Memoized since the result is stable for the request.
// Cheapest gate first: the per-site option disqualifies most sites before the more expensive purchases lookup.
$eligible = self::is_enabled_for_site()
&& ! self::was_ai_onboarded()
&& self::has_paid_plan();
Expand Down Expand Up @@ -139,9 +125,7 @@ public static function register_menu() {
return;
}

// The render callback only exists when build/build.php has been loaded,
// which happens on the AI Launchpad page itself. On other admin screens
// the menu just needs a registered slug.
// The render callback only exists once build/build.php is loaded, which happens on the AI Launchpad page itself.
$callback = function_exists( self::RENDER_CALLBACK )
? self::RENDER_CALLBACK
: '__return_empty_string';
Expand Down Expand Up @@ -205,15 +189,11 @@ public static function enqueue_jwt_initial_state() {
/**
* Fix import map ordering for the wp-build boot script.
*
* In wp-admin, _wp_footer_scripts (classic scripts) and print_import_map
* both hook into admin_print_footer_scripts at priority 10, but
* _wp_footer_scripts is registered first. This causes the inline
* import("@wordpress/boot") to execute before the import map exists.
* In wp-admin both _wp_footer_scripts and print_import_map hook admin_print_footer_scripts at priority 10, but
* _wp_footer_scripts runs first, so the inline import("@wordpress/boot") executes before the import map exists.
* This moves the import() call to a <script type="module"> printed at priority 20, after the import map.
*
* This fix moves the import() call from the classic inline script to a
* <script type="module"> printed at priority 20 (after the import map).
*
* @todo Remove once @wordpress/build ships with the loader.js fix upstream
* @todo Remove once @wordpress/build ships the loader.js fix upstream
* (WordPress/gutenberg#76870) and Jetpack updates the dependency.
*/
private static function fix_boot_import_map_ordering() {
Expand All @@ -227,7 +207,6 @@ static function () use ( $handle ) {
return;
}

// Find and extract the import("@wordpress/boot") inline script.
$boot_script = null;
$remaining = array();
foreach ( $data as $line ) {
Expand All @@ -242,7 +221,6 @@ static function () use ( $handle ) {
return;
}

// Remove from the classic script handle.
wp_scripts()->add_data( $handle, 'after', $remaining );

// Re-emit as a module script after the import map.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@
*/

/**
* Completes the About-page tasks from wp-admin when the AI Launchpad selected
* them: `add_about_page` (the AI-created About page is first published) and
* `update_about_page` (it is edited again afterwards).
* Completes the AI-selected About-page tasks from wp-admin: `add_about_page` (first published) and
* `update_about_page` (edited again afterwards).
*
* The catalog's own completion for these depends on the `_wpcom_template_layout_category`
* meta, which is provided by the dotcom editor toolkit and is not registered on
* Atomic, and on the AI's `createPatternPage` only ever creating a draft. So this
* tags the AI-created About page with its own marker meta and watches that page's
* publish/update transitions instead — independent of the layout-category meta.
* The catalog's own completion depends on the `_wpcom_template_layout_category` meta, which is not registered on
* Atomic, so this tags the AI-created About page with its own marker meta and watches that page's transitions instead.
*/
class AI_Launchpad_About_Page_Listener {

Expand All @@ -34,9 +30,9 @@ public static function register() {
}

/**
* Registers the marker meta so the block editor preserves it and the create
* request can set it. Protected (underscore-prefixed), so the auth callback
* limits writes to users who can edit pages.
* Registers the marker meta so the block editor preserves it and the create request can set it.
*
* The auth callback limits writes to users who can edit pages.
*
* @return void
*/
Expand All @@ -56,9 +52,8 @@ public static function register_meta() {
}

/**
* Completes the About-page tasks on the AI About page's status transitions:
* first publish -> add_about_page, a later edit of the published page ->
* update_about_page. Only fires for the marked page and AI-selected tasks.
* Completes the About-page tasks on the marked page's status transitions: first publish -> add_about_page,
* a later edit of the published page -> update_about_page.
*
* @param string $new_status The new post status.
* @param string $old_status The previous post status.
Expand All @@ -80,12 +75,10 @@ public static function maybe_complete( $new_status, $old_status, $post ) {
}

if ( 'publish' !== $old_status ) {
// First publish of the AI About page.
if ( in_array( 'add_about_page', $ai_task_ids, true ) ) {
wpcom_mark_launchpad_task_complete( 'add_about_page' );
}
} elseif ( in_array( 'update_about_page', $ai_task_ids, true ) ) {
// A later edit of the already-published AI About page.
wpcom_mark_launchpad_task_complete( 'update_about_page' );
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,20 @@
/**
* AI Launchpad no-CLI test-enable handler.
*
* Lets a tester turn the AI Launchpad on (and reset its state) for a site
* straight from the browser, removing the `wp option update
* wpcom_ai_launchpad_enabled 1` / reset-script SSH steps that testing otherwise
* requires. Modeled on the existing wpcom query-param overrides
* (wpcom-dashboard-redesign-override.php `?enable-dashboard-redesign=1`,
* jetpack stats `?enable_new_stats=1`), which likewise flip a feature straight
* from a `$_GET` flag.
* Lets a tester turn the AI Launchpad on (and reset its state) for a site straight from the browser.
*
* Recognized query args (on any admin page, for a `manage_options` user):
* ?enable-ai-launchpad=1 Set wpcom_ai_launchpad_enabled to 1.
* ?enable-ai-launchpad=0 Delete wpcom_ai_launchpad_enabled (turn back off).
* ?reset-ai-launchpad=1 Clear the wizard / AI-output / dismissed / task-status
* options so the wizard runs fresh.
* ?reset-ai-launchpad=1 Clear the wizard / AI-output / dismissed / task-status options so the wizard runs fresh.
*
* Each action redirects to the clean AI Launchpad page URL so a refresh does not
* re-fire it.
* Hooked on `admin_menu`, not `admin_init`: when the feature is OFF its page is unregistered and
* `user_can_access_admin_page()` dies before `admin_init`; `admin_menu` fires before that check, so the page's own
* URL can self-enable instead of dying first.
*
* Hooked on `admin_menu`, not `admin_init`: when the feature is OFF its admin
* page is unregistered, and WordPress runs the `user_can_access_admin_page()`
* check (and dies with "you are not allowed to access this page") in
* wp-admin/menu.php — which loads *before* `admin_init`. `admin_menu` fires
* inside that same file but before the access check, so handling it there lets
* the launchpad page's own URL self-enable instead of dying first.
*
* Gate: `current_user_can( 'manage_options' )` only — no nonce, matching the
* wpcom precedents, so the URL stays bookmarkable/shareable. NOTE: this ships to
* production on real sites, where it lets any paid-site admin self-enable the
* (otherwise OFF) feature on their own site. That exposure was reviewed and
* accepted as an interim testing affordance; tighten the gate before the
* controlled rollout (DOTOBRD-456) if the feature must stay invisible to
* customers.
* Gate: `current_user_can( 'manage_options' )` only — no nonce, so the URL stays bookmarkable. This ships to
* production, where it lets any paid-site admin self-enable the (otherwise OFF) feature on their own site; tighten
* the gate before the controlled rollout if the feature must stay invisible to customers.
*
* @package automattic/jetpack-mu-wpcom
*/
Expand All @@ -48,9 +31,9 @@ class AI_Launchpad_Dev_Enable {
const OPTION_ENABLED = 'wpcom_ai_launchpad_enabled';

/**
* Options cleared by a reset, matching docs/bin/reset-ai-launchpad-test-site.sh.
* The first three reference the REST controller's canonical constants so a
* rename there can't silently leave the reset clearing a stale option name.
* Options cleared by a reset.
*
* The first three reference the REST controller's constants so a rename there can't leave a stale option name here.
*/
const RESET_OPTIONS = array(
AI_Launchpad_REST::OPTION_WIZARD,
Expand All @@ -60,9 +43,7 @@ class AI_Launchpad_Dev_Enable {
);

/**
* Redirect targets returned by handle(): nothing to do, the AI Launchpad page,
* or the wp-admin dashboard. Kept as abstract tokens (not URLs) so handle() can
* be unit-tested without loading the AI Launchpad page bootstrap.
* Redirect targets returned by handle(), kept as abstract tokens (not URLs) so handle() can be unit-tested.
*/
const REDIRECT_NONE = '';
const REDIRECT_PAGE = 'page';
Expand All @@ -78,10 +59,9 @@ public static function register() {
}

/**
* Acts on the test-enable / reset query params, then redirects so a refresh
* does not re-fire the action. Disabling lands on the dashboard (the gated
* page is gone); everything else lands on the AI Launchpad page, where the
* fresh request re-registers the now-eligible menu.
* Acts on the test-enable / reset query params, then redirects so a refresh does not re-fire the action.
*
* Disabling lands on the dashboard (the gated page is gone); everything else lands on the AI Launchpad page.
*
* @return void
*/
Expand All @@ -101,11 +81,9 @@ public static function maybe_handle_request() {
}

/**
* Applies the requested option changes and returns where to send the user, as
* one of the REDIRECT_* tokens. Split from the redirect/exit — and kept free of
* the AI Launchpad page bootstrap — so it can be unit-tested in isolation. No-op
* (and cheap) on the overwhelming majority of admin requests, which carry
* neither param.
* Applies the requested option changes and returns where to send the user, as one of the REDIRECT_* tokens.
*
* Split from the redirect/exit so it can be unit-tested in isolation.
*
* @return string One of the REDIRECT_* constants (REDIRECT_NONE when there is
* nothing to do: no recognized param, or no capability).
Expand Down Expand Up @@ -142,11 +120,7 @@ public static function handle() {
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended

// Disabling removes the (eligibility-gated) launchpad page, so land on the
// dashboard rather than the now-inaccessible page. Otherwise go to the
// launchpad: the redirect starts a fresh request where the menu
// re-registers. (On a site without a paid plan the page stays gated, but
// that is out of scope — the feature requires a paid plan.)
// Disabling removes the gated launchpad page, so land on the dashboard rather than the now-inaccessible page.
return $disabling ? self::REDIRECT_DASHBOARD : self::REDIRECT_PAGE;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
*/

/**
* Registers the existing Launchpad completion listeners for the tasks the AI
* Launchpad selected, mirroring Launchpad_Task_Lists::add_hooks_for_active_tasks()
* without the fullscreen-launchpad gate. Task selection is read from the
* `wpcom_ai_launchpad_ai_output` option; completion writes go through each
* task's own `add_listener_callback` into `launchpad_checklist_tasks_statuses`.
* Registers the existing Launchpad completion listeners for the AI-selected tasks, without the fullscreen gate.
*
* Task selection is read from `wpcom_ai_launchpad_ai_output`; completion writes go through each task's own
* `add_listener_callback` into `launchpad_checklist_tasks_statuses`.
*/
class AI_Launchpad_Listeners {

Expand All @@ -25,9 +24,7 @@ public static function register() {
}

/**
* Treats AI-selected tasks as active so their shared completion callbacks
* write status even when the task is absent from the site's `site_intent`
* task list.
* Treats AI-selected tasks as active so their completion callbacks write status even when absent from `site_intent`.
*
* @param bool $is_active Whether the task is active per the site_intent task list.
* @param string $task_id The task being completed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,10 @@
*/

/**
* Recomputes the memberships task completion from Jetpack_Memberships' local
* signals, which the AI Launchpad REST read path uses instead of the catalog's
* own `is_complete_callback`s for these tasks.
* Recomputes the memberships task completion from Jetpack_Memberships' local signals.
*
* The catalog computes these from `wpcom_launchpad_get_membership_settings()`,
* which returns null under `IS_ATOMIC`, so `wpcom_launchpad_is_stripe_connected`
* / `wpcom_launchpad_has_paid_membership_plans` are always false on Atomic — and
* because those callbacks recompute (ignoring any stored option), an
* option-writing listener could not surface them. The real state is readable
* locally on Atomic, though: Jetpack_Memberships syncs the connected-account
* flag down as a site option and mirrors membership plans as the local
* `jp_mem_plan` CPT. This reads those instead.
* The catalog's own callbacks are always false on Atomic (their membership settings return null there), so the REST
* read path uses this instead. Jetpack_Memberships syncs the connected-account flag and membership plans down locally.
*/
class AI_Launchpad_Memberships {

Expand Down Expand Up @@ -59,8 +51,7 @@ public static function is_task_complete( $task_id ) {
switch ( $task_id ) {
case 'stripe_connected':
case 'set_up_payments':
// Stripe connected = a payment method is set up; wpcom completes both
// on Stripe-connect (memberships/connected-accounts.php).
// A connected account means a payment method is set up; wpcom completes both on Stripe-connect.
return (bool) Jetpack_Memberships::has_connected_account();
case 'paid_offer_created':
return (bool) Jetpack_Memberships::has_configured_plans_jetpack_recurring_payments();
Expand Down
Loading