Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
48.74% covered (danger)
48.74%
736 / 1510
3.19% covered (danger)
3.19%
3 / 94
CRAP
n/a
0 / 0
wpcom_launchpad_should_use_wp_admin_link
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_has_site_been_created_through_onboarding_flow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_get_task_definitions
77.01% covered (warning)
77.01%
576 / 748
0.00% covered (danger)
0.00%
0 / 1
24.86
wpcom_launchpad_is_site_launched
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_is_site_launched_disabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_is_woocommerce_task_completed
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
wpcom_launchpad_track_completed_task
18.18% covered (danger)
18.18%
2 / 11
0.00% covered (danger)
0.00%
0 / 1
7.93
wpcom_launchpad_get_reverse_id_mappings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
wpcom_mark_launchpad_task_complete
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
wpcom_mark_launchpad_task_incomplete
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
wpcom_launchpad_update_task_status
92.86% covered (success)
92.86%
39 / 42
0.00% covered (danger)
0.00%
0 / 1
11.04
wpcom_launchpad_init_listeners
20.00% covered (danger)
20.00%
4 / 20
0.00% covered (danger)
0.00%
0 / 1
32.09
wpcom_launchpad_init_task_definitions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
wpcom_launchpad_mark_launchpad_task_complete_if_active
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_track_edit_site_task
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_is_design_step_enabled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_domain_upsell_completed
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
wpcom_launchpad_get_domain_upsell_badge_text
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_domain_upsell_task_visible
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_mobile_app_installed
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
wpcom_launchpad_is_woocommerce_setup_visible
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
wpcom_launchpad_is_sensei_setup_visible
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_verify_domain_email_visible
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
132
wpcom_launchpad_verify_domain_email_task_displayed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_request_domains_list
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_request_user_attributes
72.22% covered (warning)
72.22%
26 / 36
0.00% covered (danger)
0.00%
0 / 1
8.05
wpcom_launchpad_are_newsletter_subscriber_counts_available
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_get_newsletter_subscriber_count
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
wpcom_launchpad_is_stripe_connected
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_has_paid_membership_plans
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
wpcom_launchpad_get_membership_settings
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_track_publish_first_post_task
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_track_migrate_content_task
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
wpcom_launchpad_get_published_non_headstart_posts_count
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
wpcom_launchpad_track_write_3_posts_task
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
wpcom_launchpad_get_write_3_posts_repetition_count
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_get_newsletter_settings_url
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_task_option_completed
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
7.90
wpcom_launchpad_is_repeated_task_complete
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
90
wpcom_launchpad_get_plan_selected_subtitle
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_get_plan_selected_badge_text
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_track_site_launch_task
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_mark_review_plugins_complete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_add_site_launch_listener
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
wpcom_launchpad_launch_task_listener_atomic
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_email_verified
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_mark_verify_email_complete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_email_task_visible
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
wpcom_launchpad_has_goal_paid_subscribers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_has_goal_import_subscribers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_is_link_in_bio_launch_disabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_is_videopress_launch_disabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_is_blog_launched_task_disabled
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_is_videopress_upload_disabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_track_video_uploaded_task
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_mark_site_title_complete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_mark_setup_site_tasks_complete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_mark_setup_general_task_complete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_mark_enable_subscribers_modal_complete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_mark_sensei_setup_complete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_domain_claim_is_visible_callback
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_domain_claim_completed
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_add_new_page_check
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
11.64
wpcom_launchpad_get_site_about_page_id
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
4.06
wpcom_launchpad_find_site_about_page_id
5.13% covered (danger)
5.13%
2 / 39
0.00% covered (danger)
0.00%
0 / 1
207.13
wpcom_launchpad_has_translation
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
wpcom_launchpad_is_add_about_page_visible
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_is_front_page_updated_visible
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_is_site_title_task_visible
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
wpcom_launchpad_add_about_page_check
36.84% covered (danger)
36.84%
7 / 19
0.00% covered (danger)
0.00%
0 / 1
48.28
wpcom_launchpad_front_page_updated_check
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
8.81
wpcom_launchpad_front_page_updated_option_check
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
wpcom_launchpad_is_update_about_page_task_visible
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_update_about_page_check
25.00% covered (danger)
25.00%
3 / 12
0.00% covered (danger)
0.00%
0 / 1
35.00
wpcom_launchpad_is_edit_page_task_visible
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_mark_customize_welcome_message_complete_on_update
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
42
wpcom_launchpad_mark_customize_welcome_message_complete_on_add
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_mark_woocommerce_setup_complete
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_edit_page_check
27.27% covered (danger)
27.27%
3 / 11
0.00% covered (danger)
0.00%
0 / 1
25.85
wpcom_launchpad_is_add_subscribe_block_visible
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_add_subscribe_block_check
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
7.33
wpcom_launchpad_domain_customize_check_purchases
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_launchpad_is_domain_customize_completed
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_mark_domain_tasks_complete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_mark_plan_tasks_complete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_trigger_email_campaign
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
6.09
wpcom_launchpad_mark_plugin_installed_complete
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_launchpad_mark_theme_selected_complete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_get_latest_draft_id
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_is_ssl_task_visible
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_launchpad_is_primary_domain_wpcom
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_get_current_site_goals_for_tracks
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
wpcom_launchpad_has_added_subscribe_block
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_launchpad_is_jetpack_social_available
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Launchpad: Task definitions and helpers
4 *
5 * @package automattic/jetpack-mu-wpcom
6 */
7
8// Type aliases used in a bunch of places in this file. Unfortunately Phan doesn't have a way to set these more globally than copy-pasting them into each file needing them.
9<<<'PHAN'
10@phan-type Task = array{id:string, title?:string, get_title?:callable, id_map?:string, add_listener_callback?:callable, badge_text_callback?:callable, extra_data_callback?:callable, get_calypso_path?:callable, is_complete_callback?:callable, is_disabled_callback?:callable, isLaunchTask?:bool, is_visible_callback?:callable, target_repetitions?:int, repetition_count_callback?:callable, subtitle?:callable, completed?:bool}
11PHAN;
12
13require_once __DIR__ . '/../../common/site-launch-publish-copy.php';
14
15/**
16 * Returns whether the task link should point to wp-admin page
17 * instead of Calypso page.
18 *
19 * @return bool
20 */
21function wpcom_launchpad_should_use_wp_admin_link() {
22    return get_option( 'wpcom_admin_interface' ) === 'wp-admin';
23}
24
25/**
26 * Returns whether the site was created through the onboarding flow.
27 *
28 * @return bool
29 */
30function wpcom_launchpad_has_site_been_created_through_onboarding_flow() {
31    return get_option( 'site_creation_flow' ) === 'onboarding';
32}
33
34/**
35 * Get the task definitions for the Launchpad.
36 *
37 * @return Task[]
38 */
39function wpcom_launchpad_get_task_definitions() {
40    $task_definitions = array(
41        // Core tasks.
42        'design_edited'                   => array(
43            'get_title'             => function () {
44                return __( 'Edit site design', 'jetpack-mu-wpcom' );
45            },
46            'id_map'                => 'site_edited',
47            'add_listener_callback' => function () {
48                add_action( 'load-site-editor.php', 'wpcom_launchpad_track_edit_site_task' );
49            },
50            'get_calypso_path'      => function () {
51                return admin_url( 'site-editor.php' );
52            },
53        ),
54        // design_completed checks for task completion while design_selected always returns true.
55        'design_completed'                => array(
56            'get_title'            => function () {
57                return __( 'Select a design', 'jetpack-mu-wpcom' );
58            },
59            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
60            'get_calypso_path'     => function ( $task, $default, $data ) {
61                if ( wpcom_launchpad_has_site_been_created_through_onboarding_flow() ) {
62                    return '/themes/' . $data['site_slug_encoded'];
63                }
64
65                $flow = get_option( 'site_intent' );
66                return '/setup/update-design/designSetup?siteSlug=' . $data['site_slug_encoded'] . '&flow=' . $flow;
67            },
68            'is_disabled_callback' => 'wpcom_launchpad_is_design_step_enabled',
69        ),
70        'design_selected'                 => array(
71            'get_title'            => function () {
72                return __( 'Select a design', 'jetpack-mu-wpcom' );
73            },
74            'is_complete_callback' => '__return_true',
75            'is_disabled_callback' => 'wpcom_launchpad_is_design_step_enabled',
76            'get_calypso_path'     => function ( $task, $default, $data ) {
77                if ( wpcom_launchpad_has_site_been_created_through_onboarding_flow() ) {
78                    return '/themes/' . $data['site_slug_encoded'];
79                }
80
81                return '/setup/update-design/designSetup?siteSlug=' . $data['site_slug_encoded'];
82            },
83        ),
84        'domain_claim'                    => array(
85            'get_title'            => function () {
86                return __( 'Claim your free one-year domain', 'jetpack-mu-wpcom' );
87            },
88            'is_complete_callback' => 'wpcom_launchpad_is_domain_claim_completed',
89            'is_visible_callback'  => 'wpcom_launchpad_domain_claim_is_visible_callback',
90            'get_calypso_path'     => function ( $task, $default, $data ) {
91                return '/domains/add/' . $data['site_slug_encoded'];
92            },
93        ),
94        'domain_upsell'                   => array(
95            'id_map'               => 'domain_upsell_deferred',
96            'get_title'            => function () {
97                return __( 'Choose a domain', 'jetpack-mu-wpcom' );
98            },
99            'is_complete_callback' => 'wpcom_launchpad_is_domain_upsell_completed',
100            'badge_text_callback'  => 'wpcom_launchpad_get_domain_upsell_badge_text',
101            'is_visible_callback'  => 'wpcom_launchpad_is_domain_upsell_task_visible',
102            'get_calypso_path'     => function ( $task, $default, $data ) {
103                if ( wpcom_launchpad_checklists()->is_task_complete( $task ) ) {
104                        return '/domains/manage/' . $data['site_slug_encoded'];
105                }
106
107                return '/setup/domain-and-plan/domains?siteSlug=' . $data['site_slug_encoded'];
108            },
109        ),
110        'first_post_published'            => array(
111            'get_title'             => function () {
112                $latest_draft_id = wpcom_launchpad_get_latest_draft_id();
113                return $latest_draft_id === false
114                        ? __( 'Write your first post', 'jetpack-mu-wpcom' )
115                        : __( 'Continue to write your first post', 'jetpack-mu-wpcom' );
116            },
117            'add_listener_callback' => function () {
118                add_action( 'publish_post', 'wpcom_launchpad_track_publish_first_post_task' );
119            },
120            'get_calypso_path'      => function () {
121                $latest_draft_id = wpcom_launchpad_get_latest_draft_id();
122
123                if ( is_int( $latest_draft_id ) ) {
124                    // There is a draft post, redirect the user to the draft instead of making a fresh post.
125                    return admin_url( 'post.php?action=edit&post=' . rawurlencode( (string) $latest_draft_id ) );
126                }
127
128                return admin_url( 'post-new.php' );
129            },
130        ),
131        'generate_content'                => array(
132            'get_title'            => function () {
133                return __( 'Customize content with AI', 'jetpack-mu-wpcom' );
134            },
135            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
136            'get_calypso_path'     => function ( $task, $default, $data ) {
137                return '/setup/readymade-template/generateContent?siteSlug=' . $data['site_slug_encoded'];
138            },
139        ),
140        'plan_completed'                  => array(
141            'get_title'            => function () {
142                return __( 'Choose a plan', 'jetpack-mu-wpcom' );
143            },
144            'subtitle'             => 'wpcom_launchpad_get_plan_selected_subtitle',
145            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
146            'badge_text_callback'  => 'wpcom_launchpad_get_plan_selected_badge_text',
147            'get_calypso_path'     => function ( $task, $default, $data ) {
148                $flow = get_option( 'site_intent' );
149                return '/setup/' . $flow . '/plans?siteSlug=' . $data['site_slug_encoded'];
150            },
151        ),
152        'plan_selected'                   => array(
153            'get_title'            => function () {
154                return __( 'Choose a plan', 'jetpack-mu-wpcom' );
155            },
156            'subtitle'             => 'wpcom_launchpad_get_plan_selected_subtitle',
157            'is_complete_callback' => '__return_true',
158            'badge_text_callback'  => 'wpcom_launchpad_get_plan_selected_badge_text',
159            'get_calypso_path'     => function ( $task, $default, $data ) {
160                return '/plans/' . $data['site_slug_encoded'];
161            },
162        ),
163        'setup_general'                   => array(
164            'get_title'            => function () {
165                return __( 'Give your site a name', 'jetpack-mu-wpcom' );
166            },
167            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
168            'is_disabled_callback' => '__return_true',
169            'get_calypso_path'     => function () {
170                return admin_url( 'options-general.php' );
171            },
172        ),
173        'site_launched'                   => array(
174            'get_title'             => function () {
175                return wpcom_site_launch_peek_publish_copy()
176                    ? __( 'Publish your site', 'jetpack-mu-wpcom' )
177                    : __( 'Launch your site', 'jetpack-mu-wpcom' );
178            },
179            'isLaunchTask'          => true,
180            'is_complete_callback'  => 'wpcom_launchpad_is_site_launched',
181            'is_disabled_callback'  => 'wpcom_launchpad_is_site_launched_disabled',
182            'add_listener_callback' => 'wpcom_launchpad_add_site_launch_listener',
183        ),
184        'verify_email'                    => array(
185            'get_title'            => function () {
186                return __( 'Verify email address', 'jetpack-mu-wpcom' );
187            },
188            'is_complete_callback' => 'wpcom_launchpad_is_email_verified',
189            'is_disabled_callback' => 'wpcom_launchpad_is_email_verified',
190            'is_visible_callback'  => 'wpcom_launchpad_is_email_task_visible',
191            'get_calypso_path'     => function () {
192                return '/me/account';
193            },
194        ),
195
196        // Newsletter pre-launch tasks.
197        'first_post_published_newsletter' => array(
198            'id_map'                => 'first_post_published',
199            'get_title'             => function () {
200                return __( 'Start writing', 'jetpack-mu-wpcom' );
201            },
202            'add_listener_callback' => function () {
203                add_action( 'publish_post', 'wpcom_launchpad_track_publish_first_post_task' );
204            },
205            'get_calypso_path'      => function () {
206                return admin_url( 'post-new.php' );
207            },
208        ),
209        'newsletter_plan_created'         => array(
210            'get_title'           => function () {
211                return __( 'Create paid Newsletter', 'jetpack-mu-wpcom' );
212            },
213            'is_visible_callback' => 'wpcom_launchpad_has_goal_paid_subscribers',
214            'get_calypso_path'    => function ( $task, $default, $data ) {
215                return '/earn/payments/' . $data['site_slug_encoded'] . '#add-newsletter-payment-plan';
216            },
217        ),
218        'setup_newsletter'                => array(
219            'id'                   => 'setup_newsletter',
220            'get_title'            => function () {
221                return __( 'Personalize newsletter', 'jetpack-mu-wpcom' );
222            },
223            'is_complete_callback' => '__return_true',
224            'get_calypso_path'     => function () {
225                return admin_url( 'options-general.php' );
226            },
227        ),
228        'set_up_payments'                 => array(
229            'get_title'           => function () {
230                return __( 'Set up payment method', 'jetpack-mu-wpcom' );
231            },
232            'is_visible_callback' => 'wpcom_launchpad_has_goal_paid_subscribers',
233            'get_calypso_path'    => function ( $task, $default, $data ) {
234                if ( function_exists( 'get_memberships_connected_account_redirect' ) ) {
235                    $user_id = get_current_user_id();
236                    $blog_id = get_current_blog_id();
237                    $source = $data['launchpad_context'];
238                    return $source
239                        ? get_memberships_connected_account_redirect( $user_id, $blog_id, $source )
240                        : get_memberships_connected_account_redirect( $user_id, $blog_id );
241                }
242                return '/earn/payments/' . $data['site_slug_encoded'];
243            },
244        ),
245        'subscribers_added'               => array(
246            'get_title'            => function () {
247                return __( 'Import subscribers', 'jetpack-mu-wpcom' );
248            },
249            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
250            'is_visible_callback'  => 'wpcom_launchpad_has_goal_import_subscribers',
251            'get_calypso_path'     => function ( $task, $default, $data ) {
252                return '/subscribers/' . $data['site_slug_encoded'] . '#add-subscribers';
253            },
254        ),
255        'migrate_content'                 => array(
256            'get_title'             => function () {
257                return __( 'Import content', 'jetpack-mu-wpcom' );
258            },
259            'is_complete_callback'  => 'wpcom_launchpad_is_task_option_completed',
260            'is_visible_callback'   => 'wpcom_launchpad_has_goal_import_subscribers',
261            'add_listener_callback' => function () {
262                add_action( 'save_post', 'wpcom_launchpad_track_migrate_content_task' );
263            },
264            'get_calypso_path'      => function ( $task, $default, $data ) {
265                if ( wpcom_launchpad_should_use_wp_admin_link() ) {
266                    return admin_url( 'import.php' );
267                }
268                return '/import/' . $data['site_slug_encoded'];
269            },
270        ),
271
272        // intent-newsletter-goal tasks
273        'start_building_your_audience'    => array(
274            'get_title'            => function () {
275                return __( 'Start building your audience', 'jetpack-mu-wpcom' );
276            },
277            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
278            'get_calypso_path'     => function ( $task, $default, $data ) {
279                return '/subscribers/' . $data['site_slug_encoded'] . '#building-your-audience-task';
280            },
281        ),
282
283        // Link in bio tasks.
284        'link_in_bio_launched'            => array(
285            'get_title'             => function () {
286                return wpcom_site_launch_peek_publish_copy()
287                    ? __( 'Publish your site', 'jetpack-mu-wpcom' )
288                    : __( 'Launch your site', 'jetpack-mu-wpcom' );
289            },
290            'id_map'                => 'site_launched',
291            'is_disabled_callback'  => 'wpcom_launchpad_is_link_in_bio_launch_disabled',
292            'add_listener_callback' => 'wpcom_launchpad_add_site_launch_listener',
293        ),
294        'links_added'                     => array(
295            'get_title'             => function () {
296                return __( 'Add links', 'jetpack-mu-wpcom' );
297            },
298            'id_map'                => 'links_edited',
299            'add_listener_callback' => function () {
300                add_action( 'load-site-editor.php', 'wpcom_launchpad_track_edit_site_task' );
301            },
302            'get_calypso_path'      => function () {
303                return admin_url( 'site-editor.php' );
304            },
305        ),
306
307        // Videopress tasks.
308        'videopress_launched'             => array(
309            'id_map'                => 'site_launched',
310            'get_title'             => function () {
311                return wpcom_site_launch_peek_publish_copy()
312                    ? __( 'Publish site', 'jetpack-mu-wpcom' )
313                    : __( 'Launch site', 'jetpack-mu-wpcom' );
314            },
315            'is_disabled_callback'  => 'wpcom_launchpad_is_videopress_launch_disabled',
316            'add_listener_callback' => 'wpcom_launchpad_add_site_launch_listener',
317        ),
318        'videopress_setup'                => array(
319            'get_title'            => function () {
320                return __( 'Set up your video site', 'jetpack-mu-wpcom' );
321            },
322            'is_complete_callback' => '__return_true',
323        ),
324        'videopress_upload'               => array(
325            'id_map'                => 'video_uploaded',
326            'get_title'             => function () {
327                return __( 'Upload your first video', 'jetpack-mu-wpcom' );
328            },
329            'is_disabled_callback'  => 'wpcom_launchpad_is_videopress_upload_disabled',
330            'add_listener_callback' => function () {
331                add_action( 'add_attachment', 'wpcom_launchpad_track_video_uploaded_task' );
332            },
333            'get_calypso_path'      => function () {
334                $page_on_front = get_option( 'page_on_front', false );
335                if ( $page_on_front ) {
336                    return admin_url( 'post.php?post=' . $page_on_front . '&action=edit' );
337                }
338                return admin_url( 'site-editor.php?canvas=edit' );
339            },
340        ),
341
342        // Blog tasks.
343        'blog_launched'                   => array(
344            'get_title'             => function () {
345                return wpcom_site_launch_peek_publish_copy()
346                    ? __( 'Publish your blog', 'jetpack-mu-wpcom' )
347                    : __( 'Launch your blog', 'jetpack-mu-wpcom' );
348            },
349            'isLaunchTask'          => true,
350            'is_disabled_callback'  => 'wpcom_launchpad_is_blog_launched_task_disabled',
351            'add_listener_callback' => 'wpcom_launchpad_add_site_launch_listener',
352        ),
353        'setup_blog'                      => array(
354            'get_title'            => function () {
355                return __( 'Name your blog', 'jetpack-mu-wpcom' );
356            },
357            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
358            'get_calypso_path'     => function ( $task, $default, $data ) {
359                $flow = get_option( 'site_intent' );
360                return '/setup/' . $flow . '/setup-blog?siteSlug=' . $data['site_slug_encoded'];
361            },
362        ),
363
364        // Free plan tasks.
365        'setup_free'                      => array(
366            'get_title'            => function () {
367                return __( 'Personalize your site', 'jetpack-mu-wpcom' );
368            },
369            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
370            'get_calypso_path'     => function () {
371                return admin_url( 'options-general.php' );
372            },
373        ),
374
375        // Publish a Blog tasks.
376        'complete_profile'                => array(
377            'get_title'            => function () {
378                return __( 'Complete your profile', 'jetpack-mu-wpcom' );
379            },
380            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
381            'get_calypso_path'     => function () {
382                return '/me#complete-your-profile';
383            },
384        ),
385
386        // Keep Building tasks.
387        'site_title'                      => array(
388            'get_title'            => function () {
389                return __( 'Give your site a name', 'jetpack-mu-wpcom' );
390            },
391            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
392            'is_visible_callback'  => 'wpcom_launchpad_is_site_title_task_visible',
393            'get_calypso_path'     => function () {
394                return admin_url( 'options-general.php' );
395            },
396        ),
397
398        'drive_traffic'                   => array(
399            'get_title'            => function () {
400                return __( 'Drive traffic to your site', 'jetpack-mu-wpcom' );
401            },
402            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
403            'is_visible_callback'  => 'wpcom_launchpad_is_jetpack_social_available',
404            'get_calypso_path'     => function () {
405                return admin_url( 'admin.php?page=jetpack-social' );
406            },
407        ),
408
409        'add_new_page'                    => array(
410            'get_title'            => function () {
411                return __( 'Add a new page', 'jetpack-mu-wpcom' );
412            },
413            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
414            'get_calypso_path'     => function () {
415                return admin_url( 'post-new.php?post_type=page' );
416            },
417        ),
418
419        'update_about_page'               => array(
420            'get_title'            => function () {
421                return __( 'Update your About page', 'jetpack-mu-wpcom' );
422            },
423            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
424            'is_visible_callback'  => 'wpcom_launchpad_is_update_about_page_task_visible',
425            'extra_data_callback'  => function () {
426                return array(
427                    'about_page_id' => wpcom_launchpad_get_site_about_page_id(),
428                );
429            },
430            'get_calypso_path'     => function () {
431                return admin_url( 'post.php?post=' . wpcom_launchpad_get_site_about_page_id() . '&action=edit' );
432            },
433        ),
434
435        'edit_page'                       => array(
436            'get_title'            => function () {
437                return __( 'Edit a page', 'jetpack-mu-wpcom' );
438            },
439            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
440            'is_visible_callback'  => 'wpcom_launchpad_is_edit_page_task_visible',
441            'get_calypso_path'     => function () {
442                return admin_url( 'edit.php?post_type=page' );
443            },
444        ),
445
446        'domain_customize'                => array(
447            'id_map'               => 'domain_customize_deferred',
448            'get_title'            => function () {
449                return __( 'Customize your domain', 'jetpack-mu-wpcom' );
450            },
451            'is_complete_callback' => 'wpcom_launchpad_is_domain_customize_completed',
452            'is_disabled_callback' => 'wpcom_launchpad_is_domain_customize_completed',
453            'get_calypso_path'     => function ( $task, $default, $data ) {
454                // The from parameter is used to redirect the user back to the Launchpad when they
455                // click on the Back button on the domain customization page.
456                // TODO: This can cause problem if this task is used in the future for other flows
457                // that are not in the Customer Home page. We should find a better way to handle this.
458                return '/domains/add/' . $data['site_slug_encoded'] . '?from=my-home';
459            },
460        ),
461
462        'share_site'                      => array(
463            'get_title'            => function () {
464                return __( 'Share your site', 'jetpack-mu-wpcom' );
465            },
466            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
467        ),
468
469        // Newsletter post-launch tasks.
470        'earn_money'                      => array(
471            'get_title'            => function () {
472                return __( 'Earn money with your newsletter', 'jetpack-mu-wpcom' );
473            },
474            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
475            'get_calypso_path'     => function ( $task, $default, $data ) {
476                return '/earn/' . $data['site_slug_encoded'];
477            },
478        ),
479
480        'customize_welcome_message'       => array(
481            'get_title'            => function () {
482                return __( 'Write a welcome message', 'jetpack-mu-wpcom' );
483            },
484            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
485            'get_calypso_path'     => 'wpcom_launchpad_get_newsletter_settings_url',
486        ),
487        'enable_subscribers_modal'        => array(
488            'get_title'            => function () {
489                return __( 'Enable subscribers modal', 'jetpack-mu-wpcom' );
490            },
491            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
492            'get_calypso_path'     => 'wpcom_launchpad_get_newsletter_settings_url',
493        ),
494        'add_10_email_subscribers'        => array(
495            'get_title'                 => function () {
496                return __( 'Get your first 10 subscribers', 'jetpack-mu-wpcom' );
497            },
498            'is_complete_callback'      => 'wpcom_launchpad_is_repeated_task_complete',
499            'is_visible_callback'       => 'wpcom_launchpad_are_newsletter_subscriber_counts_available',
500            'target_repetitions'        => 10,
501            'repetition_count_callback' => 'wpcom_launchpad_get_newsletter_subscriber_count',
502            'get_calypso_path'          => function ( $task, $default, $data ) {
503                return '/subscribers/' . $data['site_slug_encoded'];
504            },
505        ),
506        'write_3_posts'                   => array(
507            'get_title'                 => function () {
508                return __( 'Write 3 posts', 'jetpack-mu-wpcom' );
509            },
510            'repetition_count_callback' => 'wpcom_launchpad_get_write_3_posts_repetition_count',
511            'target_repetitions'        => 3,
512            'get_calypso_path'          => function () {
513                return admin_url( 'post-new.php' );
514            },
515        ),
516        'manage_subscribers'              => array(
517            'get_title'            => function () {
518                return __( 'Manage your subscribers', 'jetpack-mu-wpcom' );
519            },
520            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
521            'get_calypso_path'     => function ( $task, $default, $data ) {
522                return '/subscribers/' . $data['site_slug_encoded'];
523            },
524        ),
525        'connect_social_media'            => array(
526            'id_map'           => 'drive_traffic',
527            'get_title'        => function () {
528                return __( 'Connect your social media accounts', 'jetpack-mu-wpcom' );
529            },
530            'get_calypso_path' => function ( $task, $default, $data ) {
531                return '/marketing/connections/' . $data['site_slug_encoded'];
532            },
533        ),
534        'manage_paid_newsletter_plan'     => array(
535            'get_title'            => function () {
536                return __( 'Manage your paid Newsletter plan', 'jetpack-mu-wpcom' );
537            },
538            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
539            'is_visible_callback'  => 'wpcom_launchpad_has_goal_paid_subscribers',
540            'get_calypso_path'     => function ( $task, $default, $data ) {
541                return '/earn/payments/' . $data['site_slug_encoded'];
542            },
543        ),
544        'add_about_page'                  => array(
545            'get_title'            => function () {
546                return __( 'Add your About page', 'jetpack-mu-wpcom' );
547            },
548            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
549            'is_visible_callback'  => 'wpcom_launchpad_is_add_about_page_visible',
550            'get_calypso_path'     => function () {
551                return admin_url( 'post-new.php?post_type=page' );
552            },
553        ),
554
555        // Earn tasks
556        'stripe_connected'                => array(
557            'get_title'            => function () {
558                return __( 'Connect a Stripe account to collect payments', 'jetpack-mu-wpcom' );
559            },
560            'is_visible_callback'  => '__return_true',
561            'is_complete_callback' => 'wpcom_launchpad_is_stripe_connected',
562            'get_calypso_path'     => function ( $task, $default, $data ) {
563                if ( function_exists( 'get_memberships_connected_account_redirect' ) ) {
564                    return get_memberships_connected_account_redirect(
565                        get_current_user_id(),
566                        get_current_blog_id(),
567                        'earn-launchpad'
568                    );
569                }
570                return '/earn/payments/' . $data['site_slug_encoded'];
571            },
572        ),
573        'paid_offer_created'              => array(
574            'get_title'            => function () {
575                return __( 'Set up an offer for your supporters', 'jetpack-mu-wpcom' );
576            },
577            'is_complete_callback' => 'wpcom_launchpad_has_paid_membership_plans',
578            'is_visible_callback'  => '__return_true',
579            'get_calypso_path'     => function ( $task, $default, $data ) {
580                return '/earn/payments/' . $data['site_slug_encoded'] . '#add-new-payment-plan';
581            },
582        ),
583
584        // Hosting flow tasks
585        'site_theme_selected'             => array(
586            'get_title'            => function () {
587                return __( 'Choose a theme', 'jetpack-mu-wpcom' );
588            },
589            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
590            'get_calypso_path'     => function ( $task, $default, $data ) {
591                if ( wpcom_launchpad_should_use_wp_admin_link() ) {
592                    return admin_url( 'themes.php' );
593                }
594                return '/themes/' . $data['site_slug_encoded'] . '#theme-selected';
595            },
596        ),
597        'install_custom_plugin'           => array(
598            'get_title'            => function () {
599                return __( 'Install a custom plugin', 'jetpack-mu-wpcom' );
600            },
601            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
602            'get_calypso_path'     => function ( $task, $default, $data ) {
603                if ( wpcom_launchpad_should_use_wp_admin_link() ) {
604                    return admin_url( 'plugins.php' );
605                }
606                return '/plugins/' . $data['site_slug_encoded'] . '#install-plugin';
607            },
608        ),
609        'setup_ssh'                       => array(
610            'get_title'            => function () {
611                return __( 'Set up ssh', 'jetpack-mu-wpcom' );
612            },
613            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
614            'get_calypso_path'     => function ( $task, $default, $data ) {
615                return '/hosting-config/' . $data['site_slug_encoded'] . '#sftp-credentials';
616            },
617        ),
618        'site_monitoring_page'            => array(
619            'get_title'            => function () {
620                return __( 'View site metrics', 'jetpack-mu-wpcom' );
621            },
622            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
623            'get_calypso_path'     => function ( $task, $default, $data ) {
624                return '/site-monitoring/' . $data['site_slug_encoded'] . '#site-monitoring';
625            },
626        ),
627        'import_subscribers'              => array(
628            'get_title'            => function () {
629                return __( 'Import existing subscribers', 'jetpack-mu-wpcom' );
630            },
631            'id_map'               => 'subscribers_added',
632            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
633            'is_visible_callback'  => '__return_true',
634            'get_calypso_path'     => function ( $task, $default, $data ) {
635                return '/subscribers/' . $data['site_slug_encoded'] . '#add-subscribers';
636            },
637        ),
638        'add_subscribe_block'             => array(
639            'get_title'            => function () {
640                return __( 'Add the Subscribe Block to your site', 'jetpack-mu-wpcom' );
641            },
642            'is_complete_callback' => 'wpcom_launchpad_has_added_subscribe_block',
643            'is_visible_callback'  => 'wpcom_launchpad_is_add_subscribe_block_visible',
644            'get_calypso_path'     => 'wpcom_launchpad_get_newsletter_settings_url',
645        ),
646        'mobile_app_installed'            => array(
647            'get_title'            => function () {
648                return __( 'Install the mobile app', 'jetpack-mu-wpcom' );
649            },
650            'is_complete_callback' => 'wpcom_launchpad_is_mobile_app_installed',
651            'get_calypso_path'     => function () {
652                return '/me/get-apps';
653            },
654        ),
655        'post_sharing_enabled'            => array(
656            'get_title'            => function () {
657                return __( 'Enable post sharing', 'jetpack-mu-wpcom' );
658            },
659            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
660            'is_visible_callback'  => 'wpcom_launchpad_is_jetpack_social_available',
661            'get_calypso_path'     => function () {
662                return admin_url( 'admin.php?page=jetpack-social' );
663            },
664        ),
665        'front_page_updated'              => array(
666            'get_title'            => function () {
667                return __( "Update your site's design", 'jetpack-mu-wpcom' );
668            },
669            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
670            'is_visible_callback'  => 'wpcom_launchpad_is_front_page_updated_visible',
671            'get_calypso_path'     => function () {
672                $page_on_front = get_option( 'page_on_front', false );
673                if ( $page_on_front ) {
674                    return admin_url( 'post.php?post=' . $page_on_front . '&action=edit' );
675                }
676                return admin_url( 'site-editor.php?canvas=edit' );
677            },
678        ),
679        'woocommerce_setup'               => array(
680            'get_title'            => function () {
681                return __( 'Finish store setup', 'jetpack-mu-wpcom' );
682            },
683            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
684            'is_visible_callback'  => 'wpcom_launchpad_is_woocommerce_setup_visible',
685            'get_calypso_path'     => function () {
686                return site_url( '/wp-admin/admin.php?page=wc-admin' );
687            },
688        ),
689        'sensei_setup'                    => array(
690            'get_title'            => function () {
691                return __( 'Finish Sensei setup', 'jetpack-mu-wpcom' );
692            },
693            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
694            'is_visible_callback'  => 'wpcom_launchpad_is_sensei_setup_visible',
695            'get_calypso_path'     => function () {
696                return site_url( '/wp-admin/admin.php?page=sensei' );
697            },
698        ),
699        'verify_domain_email'             => array(
700            'get_title'            => function () {
701                return __( 'Verify the email address for your domains', 'jetpack-mu-wpcom' );
702            },
703            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
704            'is_visible_callback'  => 'wpcom_launchpad_is_verify_domain_email_visible',
705            'get_calypso_path'     => function ( $task, $default, $data ) {
706                return '/domains/manage/' . $data['site_slug_encoded'];
707            },
708        ),
709
710        // WooCommerce tasks
711        'woo_customize_store'             => array(
712            'get_title'            => function () {
713                return __( 'Customize your store', 'jetpack-mu-wpcom' );
714            },
715            'is_complete_callback' => 'wpcom_launchpad_is_woocommerce_task_completed',
716            'is_visible_callback'  => 'wpcom_launchpad_is_woocommerce_setup_visible',
717            'get_calypso_path'     => function () {
718                return site_url( '/wp-admin/admin.php?page=wc-admin&path=%2Fcustomize-store' );
719            },
720        ),
721        'woo_products'                    => array(
722            'get_title'            => function () {
723                return __( 'Add your products', 'jetpack-mu-wpcom' );
724            },
725            'is_complete_callback' => 'wpcom_launchpad_is_woocommerce_task_completed',
726            'is_visible_callback'  => 'wpcom_launchpad_is_woocommerce_setup_visible',
727            'get_calypso_path'     => function () {
728                return site_url( '/wp-admin/admin.php?page=wc-admin&task=products' );
729            },
730        ),
731        'woo_woocommerce_payments'        => array(
732            'get_title'            => function () {
733                return __( 'Get paid with WooPayments', 'jetpack-mu-wpcom' );
734            },
735            'is_complete_callback' => 'wpcom_launchpad_is_woocommerce_task_completed',
736            'is_visible_callback'  => 'wpcom_launchpad_is_woocommerce_setup_visible',
737            'get_calypso_path'     => function () {
738                return site_url( '/wp-admin/admin.php?page=wc-admin&task=woocommerce-payments' );
739            },
740        ),
741        'woo_tax'                         => array(
742            'get_title'            => function () {
743                return __( 'Collect sales tax', 'jetpack-mu-wpcom' );
744            },
745            'is_complete_callback' => 'wpcom_launchpad_is_woocommerce_task_completed',
746            'is_visible_callback'  => 'wpcom_launchpad_is_woocommerce_setup_visible',
747            'get_calypso_path'     => function () {
748                return site_url( '/wp-admin/admin.php?page=wc-admin&task=tax' );
749            },
750        ),
751        'woo_marketing'                   => array(
752            'get_title'            => function () {
753                return __( 'Grow your business', 'jetpack-mu-wpcom' );
754            },
755            'is_complete_callback' => 'wpcom_launchpad_is_woocommerce_task_completed',
756            'is_visible_callback'  => 'wpcom_launchpad_is_woocommerce_setup_visible',
757            'get_calypso_path'     => function () {
758                return site_url( '/wp-admin/admin.php?page=wc-admin&task=marketing' );
759            },
760        ),
761        'woo_add_domain'                  => array(
762            'get_title'            => function () {
763                return __( 'Add a domain', 'jetpack-mu-wpcom' );
764            },
765            'is_complete_callback' => 'wpcom_launchpad_is_woocommerce_task_completed',
766            'is_visible_callback'  => 'wpcom_launchpad_is_woocommerce_setup_visible',
767            'get_calypso_path'     => function ( $task, $default, $data ) {
768                return '/domains/add/' . $data['site_slug_encoded'];
769            },
770        ),
771        'woo_launch_site'                 => array(
772            'get_title'            => function () {
773                return __( 'Launch your store', 'jetpack-mu-wpcom' );
774            },
775            'is_complete_callback' => 'wpcom_launchpad_is_woocommerce_task_completed',
776            'is_visible_callback'  => 'wpcom_launchpad_is_woocommerce_setup_visible',
777            'get_calypso_path'     => function () {
778                return site_url( '/wp-admin/admin.php?page=wc-admin&path=%2Flaunch-your-store' );
779            },
780        ),
781        'migrating_site'                  => array(
782            'get_title'            => function () {
783                return __( 'Migrating the site', 'jetpack-mu-wpcom' );
784            },
785            'is_complete_callback' => '__return_true',
786            'is_visible_callback'  => '__return_true',
787        ),
788        // Post-migration tasks.
789        'review_site'                     => array(
790            'get_title'            => function () {
791                return __( "Review the site's content", 'jetpack-mu-wpcom' );
792            },
793            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
794            'is_visible_callback'  => '__return_true',
795        ),
796        'review_plugins'                  => array(
797            'get_title'             => function () {
798                return __( 'Review the migrated plugins', 'jetpack-mu-wpcom' );
799            },
800            'is_complete_callback'  => 'wpcom_launchpad_is_task_option_completed',
801            'is_visible_callback'   => '__return_true',
802            'add_listener_callback' => function () {
803                add_action( 'pre_current_active_plugins', 'wpcom_launchpad_mark_review_plugins_complete' );
804            },
805            'get_calypso_path'      => function () {
806                return admin_url( 'plugins.php' );
807            },
808        ),
809        'connect_migration_domain'        => array(
810            'get_title'            => function () {
811                return __( 'Connect your domain', 'jetpack-mu-wpcom' );
812            },
813            'get_calypso_path'     => function ( $task, $default, $data ) {
814                $site_id = get_current_blog_id();
815                // Attempt to get the domain from the pre-transfer site option if the function exists, otherwise check the current site option.
816                // @phan-suppress-next-line PhanUndeclaredFunction -- Being checked before being called.
817                $domain = function_exists( 'wpcom_get_migration_source_site_domain' ) ? wpcom_get_migration_source_site_domain( $site_id ) : get_option( 'migration_source_site_domain', null );
818                $path   = $domain ? '/domains/add/use-my-domain/' . $data['site_slug_encoded'] . '/?initialQuery=' . $domain : '/domains/add/use-my-domain/' . $data['site_slug_encoded'];
819                return $path;
820            },
821            'is_complete_callback' => 'wpcom_launchpad_is_domain_customize_completed',
822            'is_visible_callback'  => '__return_true',
823        ),
824        'domain_dns_mapped'               => array(
825            'get_title'            => function () {
826                return __( "Update your domain's DNS", 'jetpack-mu-wpcom' );
827            },
828            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
829            'is_visible_callback'  => '__return_true',
830        ),
831        'check_ssl_status'                => array(
832            'get_title'            => function () {
833                return __( 'Provision SSL certificate', 'jetpack-mu-wpcom' );
834            },
835            'is_complete_callback' => 'wpcom_launchpad_is_task_option_completed',
836            'is_visible_callback'  => 'wpcom_launchpad_is_ssl_task_visible',
837            'get_calypso_path'     => function ( $task, $default, $data ) {
838                $domain = $data['site_slug_encoded'];
839                return '/domains/manage/' . $domain . '/edit/' . $domain;
840            },
841        ),
842    );
843
844    $extended_task_definitions = apply_filters( 'wpcom_launchpad_extended_task_definitions', array() );
845
846    return array_merge( $extended_task_definitions, $task_definitions );
847}
848
849/**
850 * Returns true if the current site is launched.
851 *
852 * @param Task $task The task object.
853 * @param bool $is_complete The current task status.
854 *
855 * @return boolean
856 */
857function wpcom_launchpad_is_site_launched( $task, $is_complete ) {
858    if ( $is_complete ) {
859        return true;
860    }
861
862    $launch_status = get_option( 'launch-status' );
863
864    if ( 'launched' === $launch_status ) {
865        wpcom_mark_launchpad_task_complete( 'site_launched' );
866        return true;
867    } else {
868        return false;
869    }
870}
871
872/**
873 * Disabled when the site is already launched.
874 *
875 * @return boolean
876 */
877function wpcom_launchpad_is_site_launched_disabled() {
878    return 'launched' === get_option( 'launch-status' );
879}
880
881/**
882 * Returns true if one of the site's WooCommerce tasks is complete.
883 *
884 * @param Task $task The task object.
885 * @param bool $is_complete The current task status.
886 *
887 * @return boolean
888 */
889function wpcom_launchpad_is_woocommerce_task_completed( $task, $is_complete ) {
890    if ( $is_complete ) {
891        return true;
892    }
893
894    // Array mapping task 'id' from $task to keys used in 'woocommerce_task_list_tracked_completed_tasks' site option
895    $task_map = array(
896        'woo_customize_store'      => 'customize-store',
897        'woo_products'             => 'products',
898        'woo_woocommerce_payments' => 'woocommerce-payments',
899        'woo_tax'                  => 'tax',
900        'woo_marketing'            => 'marketing',
901        'woo_add_domain'           => 'add_domain',
902        'woo_launch_site'          => 'launch_site',
903    );
904
905    $completed_tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() );
906
907    if ( array_key_exists( $task['id'], $task_map ) && in_array( $task_map[ $task['id'] ], $completed_tasks, true ) ) {
908        wpcom_mark_launchpad_task_complete( $task['id'] );
909        return true;
910    } else {
911        return false;
912    }
913}
914
915/**
916 * Record completion event in Tracks if we're running on WP.com.
917 *
918 * @param string       $task_id The task ID.
919 * @param array        $extra_props Optional extra arguments to pass to the Tracks event.
920 * @param WP_User|null $user Optional user to use instead of the current user.
921 *
922 * @return void
923 */
924function wpcom_launchpad_track_completed_task( $task_id, $extra_props = array(), $user = null ) {
925    if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
926        return;
927    }
928
929    require_lib( 'tracks/client' );
930
931    tracks_record_event(
932        $user ?? wp_get_current_user(),
933        'wpcom_launchpad_mark_task_complete',
934        array_merge(
935            array( 'task_id' => $task_id ),
936            $extra_props
937        )
938    );
939}
940
941/**
942 * Returns a list of mappings from 'id_map' to the actual task id.
943 *
944 * @return array
945 */
946function wpcom_launchpad_get_reverse_id_mappings() {
947    $task_definitions = wpcom_launchpad_get_task_definitions();
948
949    $mapping = array();
950    foreach ( $task_definitions as $task_id => $value ) {
951        if ( ! isset( $value['id_map'] ) ) {
952            continue;
953        }
954        $mapping[ $value['id_map'] ] = $task_id;
955    }
956
957    return $mapping;
958}
959/**
960 * Mark a task as complete.
961 *
962 * Also included wp-content/lib/memberships/connected-accounts.php#36
963 * and wp-content/lib/memberships/class.membership-product.php#497
964 *
965 * @param string $task_id The task ID that should be marked as complete.
966 *
967 * @return bool True if the task was marked as complete, false otherwise.
968 */
969function wpcom_mark_launchpad_task_complete( $task_id ) {
970    if ( empty( $task_id ) ) {
971        return false;
972    }
973
974    $result = wpcom_launchpad_update_task_status( array( $task_id => true ) );
975
976    return isset( $result[ $task_id ] ) && $result[ $task_id ];
977}
978
979/**
980 * Mark a task as incomplete.
981 *
982 * @param string $task_id The task ID that should be marked as incomplete.
983 * @return bool True if the task was marked as incomplete, false otherwise.
984 */
985function wpcom_mark_launchpad_task_incomplete( $task_id ) {
986    if ( empty( $task_id ) ) {
987        return false;
988    }
989
990    $result = wpcom_launchpad_update_task_status( array( $task_id => false ) );
991
992    return isset( $result[ $task_id ] ) && ! $result[ $task_id ];
993}
994
995/**
996 * Updates task/s statuses. If a non-existent task is passed, it will be ignored.
997 *
998 * @param bool[] $new_statuses Array of mappings [ task_id: string => new_status: bool ].
999 * @return bool[] Return the new values of the requested statuses with the requested task ID as the key. This will be an empty array if the option update fails, and any ignored status values will not be returned.
1000 */
1001function wpcom_launchpad_update_task_status( $new_statuses ) {
1002    if ( ! is_array( $new_statuses ) ) {
1003        return array();
1004    }
1005
1006    $task_definitions = wpcom_launchpad_get_task_definitions();
1007    $reverse_id_map   = wpcom_launchpad_get_reverse_id_mappings();
1008
1009    $statuses_to_update = array();
1010    $response_statuses  = array();
1011    $option_map         = array();
1012
1013    foreach ( $new_statuses as $requested_task_id => $new_status ) {
1014        // Work out what the underlying task ID/option is.
1015        if ( isset( $task_definitions[ $requested_task_id ] ) ) {
1016            // Check if the requested task has an id_map.
1017            if ( isset( $task_definitions[ $requested_task_id ]['id_map'] ) ) {
1018                $resolved_task_id = $task_definitions[ $requested_task_id ]['id_map'];
1019            } else {
1020                $resolved_task_id = $requested_task_id;
1021            }
1022        } elseif ( isset( $reverse_id_map[ $requested_task_id ] ) ) {
1023            // We have some tasks that are only an id_map, but not a standalone task ID.
1024            $resolved_task_id = $requested_task_id;
1025        } else {
1026            continue;
1027        }
1028
1029        $new_status = (bool) $new_status;
1030
1031        $statuses_to_update[ $resolved_task_id ] = $new_status;
1032        $response_statuses[ $requested_task_id ] = $new_status;
1033        $option_map[ $resolved_task_id ]         = $requested_task_id;
1034    }
1035
1036    $old_values = (array) get_option( 'launchpad_checklist_tasks_statuses', array() );
1037    // Only store truthy values.
1038    $new_values = array_filter(
1039        array_merge( $old_values, $statuses_to_update )
1040    );
1041
1042    // Check for a no-op where we are not actually writing anything.
1043    if ( $new_values === $old_values ) {
1044        return $response_statuses;
1045    }
1046
1047    if ( empty( $new_values ) ) {
1048        // If the new value is empty, but we had values before, we need to delete the option.
1049        $update_result = delete_option( 'launchpad_checklist_tasks_statuses' );
1050    } else {
1051        $update_result = update_option( 'launchpad_checklist_tasks_statuses', $new_values );
1052    }
1053
1054    if ( ! $update_result ) {
1055        return array();
1056    }
1057
1058    // Track task completion for newly completed tasks.
1059    $maybe_newly_completed_tasks = array_filter( $statuses_to_update );
1060
1061    foreach ( $maybe_newly_completed_tasks as $task_id => $task_status ) {
1062        if ( isset( $old_values[ $task_id ] ) ) {
1063            // Task was already complete - no need to mark as complete again.
1064            continue;
1065        }
1066        // Use the requested task ID for completion tracking.
1067        $requested_task_id = $option_map[ $task_id ];
1068
1069        wpcom_launchpad_track_completed_task(
1070            $requested_task_id,
1071            array(
1072                'goals' => wpcom_get_current_site_goals_for_tracks(),
1073            )
1074        );
1075    }
1076
1077    return $response_statuses;
1078}
1079
1080/**
1081 * Initialize the Launchpad task listener callbacks.
1082 *
1083 * @param Task[] $task_definitions The tasks to initialize.
1084 *
1085 * @return mixed void or WP_Error.
1086 */
1087function wpcom_launchpad_init_listeners( $task_definitions ) {
1088    foreach ( $task_definitions as $task_id => $task_definition ) {
1089        if ( isset( $task_definition['add_listener_callback'] ) && is_callable( $task_definition['add_listener_callback'] ) ) {
1090            $task_data = array_merge( $task_definition, array( 'id' => $task_id ) );
1091
1092            try {
1093                call_user_func( $task_definition['add_listener_callback'], $task_data ); // Current callbacks expect the built, registered task for the second parameter, which won't work in this case.
1094            } catch ( Exception $e ) {
1095                if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1096                    require_once WP_CONTENT_DIR . '/lib/log2logstash/log2logstash.php';
1097
1098                    $data = array(
1099                        'blog_id'     => get_current_blog_id(),
1100                        'task_id'     => $task_id,
1101                        'site_intent' => get_option( 'site_intent' ),
1102                    );
1103
1104                    log2logstash(
1105                        array(
1106                            'feature' => 'launchpad',
1107                            'message' => 'Launchpad failed to add listener callback.',
1108                            'extra'   => wp_json_encode( $data, JSON_UNESCAPED_SLASHES ),
1109                        )
1110                    );
1111                }
1112
1113                return new WP_Error( 'launchpad_add_listener_callback_failed', $e->getMessage() );
1114            }
1115        }
1116    }
1117}
1118
1119/**
1120 * Initialize the Launchpad task definitions.
1121 *
1122 * @return void
1123 */
1124function wpcom_launchpad_init_task_definitions() {
1125    $task_definitions = wpcom_launchpad_get_task_definitions();
1126
1127    wpcom_launchpad_init_listeners( $task_definitions );
1128}
1129add_action( 'init', 'wpcom_launchpad_init_task_definitions', 11 );
1130
1131/**
1132 * Task callbacks.
1133 */
1134
1135/**
1136 * Marks a task as complete if it is active for this site. This is a bit of a hacky way to be able to share a callback
1137 * among several tasks, calling several completion IDs from the same callback.
1138 *
1139 * @param string $task_id The task ID.
1140 * @return bool True if successful, false if not.
1141 */
1142function wpcom_launchpad_mark_launchpad_task_complete_if_active( $task_id ) {
1143    if ( wpcom_launchpad_checklists()->mark_task_complete_if_active( $task_id ) ) {
1144        wpcom_launchpad_track_completed_task(
1145            $task_id,
1146            array(
1147                'goals' => wpcom_get_current_site_goals_for_tracks(),
1148            )
1149        );
1150        return true;
1151    }
1152
1153    return false;
1154}
1155
1156/**
1157 * Callback for completing edit site task.
1158 *
1159 * @return void
1160 */
1161function wpcom_launchpad_track_edit_site_task() {
1162    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'links_added' );
1163    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'design_edited' );
1164}
1165
1166/**
1167 * Callback for design task enabled state
1168 *
1169 * @return boolean
1170 */
1171function wpcom_launchpad_is_design_step_enabled() {
1172    if ( wpcom_launchpad_has_site_been_created_through_onboarding_flow() ) {
1173        return false;
1174    }
1175
1176    return ! wpcom_can_update_design_selected_task();
1177}
1178
1179/**
1180 * Determines whether or not domain upsell task is completed.
1181 *
1182 * @param Task  $task    The Task object.
1183 * @param mixed $default The default value.
1184 * @return bool True if domain upsell task is completed.
1185 */
1186function wpcom_launchpad_is_domain_upsell_completed( $task, $default ) {
1187    if ( wpcom_site_has_feature( 'custom-domain' ) ) {
1188        return true;
1189    }
1190
1191    if ( function_exists( 'wpcom_get_site_purchases' ) ) {
1192        $site_purchases = wpcom_get_site_purchases();
1193
1194        // Check if the site has any domain purchases.
1195        $domain_purchases = array_filter(
1196            $site_purchases,
1197            function ( $site_purchase ) {
1198                return in_array( $site_purchase->product_type, array( 'domain_map', 'domain_reg' ), true );
1199            }
1200        );
1201
1202        if ( ! empty( $domain_purchases ) ) {
1203            return true;
1204        }
1205    }
1206
1207    return $default;
1208}
1209
1210/**
1211 * Returns the badge text for the domain upsell task
1212 *
1213 * @return string Badge text
1214 */
1215function wpcom_launchpad_get_domain_upsell_badge_text() {
1216    // Never run `wpcom_is_checklist_task_complete` within a is_complete_callback unless you are fond of infinite loops.
1217    return wpcom_is_checklist_task_complete( 'domain_upsell' ) ? '' : __( 'Upgrade plan', 'jetpack-mu-wpcom' );
1218}
1219
1220/**
1221 * Determines whether or not domain upsell task should be visible.
1222 *
1223 * @return bool True if user is on a free plan.
1224 */
1225function wpcom_launchpad_is_domain_upsell_task_visible() {
1226    if ( ! function_exists( 'wpcom_get_site_purchases' ) ) {
1227        return false;
1228    }
1229
1230    $site_purchases = wpcom_get_site_purchases();
1231
1232    $bundle_purchases = array_filter(
1233        $site_purchases,
1234        function ( $site_purchase ) {
1235            return $site_purchase->product_type === 'bundle';
1236        }
1237    );
1238
1239    return empty( $bundle_purchases );
1240}
1241
1242/**
1243 * Verifies if the Mobile App is installed for the current user.
1244 *
1245 * @param Task $task The task object.
1246 * @param bool $is_complete The current task status.
1247 * @return bool True if the Mobile App is installed for the current user.
1248 */
1249function wpcom_launchpad_is_mobile_app_installed( $task, $is_complete ) {
1250    if ( $is_complete ) {
1251        return true;
1252    }
1253
1254    $has_used_jetpack_app = null;
1255    $is_atomic_site       = ( new Automattic\Jetpack\Status\Host() )->is_woa_site();
1256    if ( $is_atomic_site ) {
1257        $user_attributes = wpcom_launchpad_request_user_attributes( array( 'has_used_jetpack_app' ) );
1258        if ( is_wp_error( $user_attributes ) ) {
1259            return false;
1260        }
1261
1262        if ( ! isset( $user_attributes['has_used_jetpack_app'] ) ) {
1263            return false;
1264        }
1265
1266        $has_used_jetpack_app = $user_attributes['has_used_jetpack_app'];
1267    } else {
1268        if ( ! function_exists( 'get_user_attribute' ) ) {
1269            return false;
1270        }
1271
1272        $user_id              = get_current_user_id();
1273        $has_used_jetpack_app = get_user_attribute( $user_id, 'jp_mobile_app_last_seen' );
1274    }
1275
1276    if ( empty( $has_used_jetpack_app ) ) {
1277        return false;
1278    }
1279
1280    wpcom_mark_launchpad_task_complete( 'mobile_app_installed' );
1281
1282    return true;
1283}
1284
1285/**
1286 * Determines whether or not the WooCommerce setup task should be visible.
1287 *
1288 * @param Task $task The task object.
1289 * @return bool True if the site is a WOA site and WooCommerce is active.
1290 */
1291function wpcom_launchpad_is_woocommerce_setup_visible( $task ) {
1292    // Get current plan
1293    $is_ecommerce_trial_plan = false;
1294    if ( function_exists( 'wpcom_get_site_purchases' ) ) {
1295        $purchases = wpcom_get_site_purchases();
1296        foreach ( $purchases as $purchase ) {
1297            if ( 'ecommerce-trial-bundle-monthly' === $purchase->product_slug ) {
1298                $is_ecommerce_trial_plan = true;
1299                break;
1300            }
1301        }
1302    }
1303
1304    // Hide these tasks in ecommerce trial plan
1305    if ( in_array( $task['id'], array( 'woo_marketing', 'woo_launch_site' ), true ) && $is_ecommerce_trial_plan ) {
1306        return false;
1307    }
1308
1309    $is_atomic_site = ( new Automattic\Jetpack\Status\Host() )->is_woa_site();
1310    if ( ! $is_atomic_site ) {
1311        return false;
1312    }
1313
1314    return is_plugin_active( 'woocommerce/woocommerce.php' );
1315}
1316
1317/**
1318 * Determines wheter or not the sensei setup task should be visible.
1319 *
1320 * @return bool True if the sensei setup task should be visible.
1321 */
1322function wpcom_launchpad_is_sensei_setup_visible() {
1323    $is_atomic_site = ( new Automattic\Jetpack\Status\Host() )->is_woa_site();
1324    if ( ! $is_atomic_site ) {
1325        return false;
1326    }
1327
1328    return is_plugin_active( 'sensei-lms/sensei-lms.php' );
1329}
1330
1331/**
1332 * Determines whether or not the verify domain email task should be visible.
1333 *
1334 * @return bool True if the verify domain email task should be visible.
1335 */
1336function wpcom_launchpad_is_verify_domain_email_visible() {
1337    // If the task is complete, we should show it and prevent the logic below
1338    // to be executed.
1339    if ( wpcom_is_checklist_task_complete( 'verify_domain_email' ) ) {
1340        return true;
1341    }
1342
1343    $domains_pending_icann_verification = array();
1344
1345    // For Atomic sites we need to get the domain list from
1346    // the public API.
1347    $is_atomic_site = ( new Automattic\Jetpack\Status\Host() )->is_woa_site();
1348    if ( $is_atomic_site ) {
1349        $domains = wpcom_launchpad_request_domains_list();
1350
1351        if ( is_wp_error( $domains ) ) {
1352            return false;
1353        }
1354
1355        $domains_pending_icann_verification = array_filter(
1356            $domains,
1357            function ( $domain ) {
1358                return isset( $domain->is_pending_icann_verification ) && $domain->is_pending_icann_verification;
1359            }
1360        );
1361    } else {
1362        if ( ! class_exists( 'Domain_Management' ) ) {
1363            return false;
1364        }
1365
1366        $domains = \Domain_Management::get_paid_domains_with_icann_verification_status();
1367
1368        $domains_pending_icann_verification = array_filter(
1369            $domains,
1370            function ( $domain ) {
1371                return isset( $domain['is_pending_icann_verification'] ) && $domain['is_pending_icann_verification'];
1372            }
1373        );
1374    }
1375
1376    $has_domains_pending_icann_verification = ! empty( $domains_pending_icann_verification );
1377
1378    if ( ! $has_domains_pending_icann_verification && wpcom_launchpad_verify_domain_email_task_displayed() ) {
1379        wpcom_mark_launchpad_task_complete( 'verify_domain_email' );
1380        return true;
1381    }
1382
1383    if ( $has_domains_pending_icann_verification ) {
1384        if ( ! wpcom_launchpad_verify_domain_email_task_displayed() ) {
1385            wpcom_set_launchpad_config_option( 'verify_domain_email_task_displayed', true );
1386        }
1387        return true;
1388    }
1389
1390    return false;
1391}
1392
1393/**
1394 * Checks if the Verify Email Domain task was displayed to the user.
1395 */
1396function wpcom_launchpad_verify_domain_email_task_displayed() {
1397    return wpcom_get_launchpad_config_option( 'verify_domain_email_task_displayed', false );
1398}
1399
1400/**
1401 * Make a request to the WordPress.com API to get the domain list for the current site.
1402 *
1403 * @return array|WP_Error Array of domains and their verification status or WP_Error if the request fails.
1404 */
1405function wpcom_launchpad_request_domains_list() {
1406    // Use a static variable as a temporary in-memory cache to avoid multiple outbound
1407    // HTTP requests within a single incoming request.
1408    // We don't expect this to be triggered multiple times, but it's worth adding some
1409    // light caching to avoid multiple, possibly slow HTTP requests where the underlying data
1410    // is highly unlikely to change.
1411    // The "cache" only lasts as long as the current request/memory space, so we don't need to invalidate it.
1412    static $cached_domains = null;
1413
1414    if ( $cached_domains !== null ) {
1415        return $cached_domains;
1416    }
1417
1418    $site_id       = \Jetpack_Options::get_option( 'id' );
1419    $request_path  = sprintf( '/sites/%d/domains', $site_id );
1420    $wpcom_request = Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_blog(
1421        $request_path,
1422        '1.2',
1423        array(
1424            'method'  => 'GET',
1425            'headers' => array(
1426                'content-type'    => 'application/json',
1427                'X-Forwarded-For' => ( new Automattic\Jetpack\Status\Visitor() )->get_ip( true ),
1428            ),
1429        ),
1430        null,
1431        'rest'
1432    );
1433
1434    $response_code = wp_remote_retrieve_response_code( $wpcom_request );
1435    if ( 200 !== $response_code ) {
1436        return new \WP_Error(
1437            'failed_to_fetch_data',
1438            esc_html__( 'Unable to fetch the requested data.', 'jetpack-mu-wpcom' ),
1439            array( 'status' => $response_code )
1440        );
1441    }
1442
1443    $body         = wp_remote_retrieve_body( $wpcom_request );
1444    $decoded_body = json_decode( $body );
1445
1446    if ( ! isset( $decoded_body->domains ) || ! is_array( $decoded_body->domains ) ) {
1447        return new \WP_Error(
1448            'failed_to_fetch_data',
1449            esc_html__( 'Unable to fetch the requested data.', 'jetpack-mu-wpcom' )
1450        );
1451    }
1452
1453    $cached_domains = $decoded_body->domains;
1454
1455    return $cached_domains;
1456}
1457
1458/**
1459 * Make a request to the WordPress.com API to get the values of the user
1460 * attributes sent by the client.
1461 *
1462 * @param string[]                                  $attributes The attributes to request.
1463 * @param Automattic\Jetpack\Connection\Client|null $client_wrapper Optional client wrapper to use, this is only used for testing.
1464 * @return array|WP_Error Array of user attributes or WP_Error if the request fails.
1465 */
1466function wpcom_launchpad_request_user_attributes( $attributes, $client_wrapper = null ) {
1467    if ( ! is_array( $attributes ) || $attributes === array() ) {
1468        return array();
1469    }
1470    // Use a static variable as a temporary in-memory cache to avoid multiple outbound
1471    // HTTP requests within a single incoming request.
1472    // We don't expect this to be triggered multiple times, but it's worth adding some
1473    // light caching to avoid multiple, possibly slow HTTP requests where the underlying data
1474    // is highly unlikely to change.
1475    // The "cache" only lasts as long as the current request/memory space, so we don't need to invalidate it.
1476    static $cached_attributes = array();
1477
1478    // Check if all requested attributes are available in the cache
1479    $resolved_values = array_intersect_key( $cached_attributes, array_flip( $attributes ) );
1480    if ( count( $resolved_values ) === count( $attributes ) ) {
1481        return $resolved_values;
1482    }
1483
1484    $attributes_to_fetch = array_diff( $attributes, array_keys( $resolved_values ) );
1485
1486    $query_params  = build_query( array( 'attributes' => $attributes_to_fetch ) );
1487    $client        = $client_wrapper ? $client_wrapper : new Automattic\Jetpack\Connection\Client();
1488    $wpcom_request = $client->wpcom_json_api_request_as_user(
1489        '/jetpack-user-attributes?' . $query_params,
1490        'v2',
1491        array(
1492            'method'  => 'GET',
1493            'headers' => array(
1494                'X-Forwarded-For' => ( new Automattic\Jetpack\Status\Visitor() )->get_ip( true ),
1495            ),
1496        )
1497    );
1498
1499    $response_code = wp_remote_retrieve_response_code( $wpcom_request );
1500    if ( 200 !== $response_code ) {
1501        return new \WP_Error(
1502            'failed_to_fetch_data',
1503            esc_html__( 'Unable to fetch the requested data.', 'jetpack-mu-wpcom' ),
1504            array( 'status' => $response_code )
1505        );
1506    }
1507
1508    $body         = wp_remote_retrieve_body( $wpcom_request );
1509    $decoded_body = json_decode( $body );
1510
1511    if ( ! isset( $decoded_body->user_attributes ) ) {
1512        return new \WP_Error(
1513            'failed_to_fetch_data',
1514            esc_html__( 'Unable to fetch the requested data.', 'jetpack-mu-wpcom' )
1515        );
1516    }
1517
1518    $user_attributes = get_object_vars( $decoded_body->user_attributes );
1519
1520    $cached_attributes = array_merge( $cached_attributes, $user_attributes );
1521
1522    return $cached_attributes;
1523}
1524
1525/**
1526 * Identify whether we can retrieve newsletter subscriber counts in
1527 * the current environment.
1528 *
1529 * @return bool Whether or not we can compute the number of newsletter subscribers.
1530 */
1531function wpcom_launchpad_are_newsletter_subscriber_counts_available() {
1532    // If we aren't running on WordPress.com, we can't access blog subscriber information accurately.
1533    if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
1534        return false;
1535    }
1536
1537    // Note that these functions are used in wpcom_launchpad_get_newsletter_subscriber_count(),
1538    // so the list of functions needs to be kept in sync with that code.
1539    if ( ! function_exists( 'wpcom_subs_total_for_blog' ) || ! function_exists( 'wpcom_subs_is_subscribed' ) ) {
1540        return false;
1541    }
1542
1543    return true;
1544}
1545
1546/**
1547 * Get the number of newsletter subscribers.
1548 *
1549 * @return int The number of newsletter subscribers for the current blog.
1550 */
1551function wpcom_launchpad_get_newsletter_subscriber_count() {
1552    // Check whether we can compute the subscriber counts first.
1553    // If we add any WordPress.com function calls in this code, we
1554    // need to add checks in that function.
1555    if ( ! wpcom_launchpad_are_newsletter_subscriber_counts_available() ) {
1556        return 0;
1557    }
1558
1559    $current_blog_id = get_current_blog_id();
1560
1561    $total_subscribers = wpcom_subs_total_for_blog( $current_blog_id );
1562
1563    // Account for the fact that the admin user is a subscriber by default.
1564    $is_subscribed_arguments = array(
1565        'blog_id' => $current_blog_id,
1566        'user_id' => get_current_user_id(),
1567    );
1568    if ( $total_subscribers > 0 && wpcom_subs_is_subscribed( $is_subscribed_arguments ) ) {
1569        --$total_subscribers;
1570    }
1571
1572    return (int) $total_subscribers;
1573}
1574
1575/**
1576 * Determines if Stripe has been connected.
1577 *
1578 * @return bool Whether Stripe account is connected.
1579 */
1580function wpcom_launchpad_is_stripe_connected() {
1581    $membership_settings = wpcom_launchpad_get_membership_settings();
1582    if ( ! $membership_settings ) {
1583        return false;
1584    }
1585    return isset( $membership_settings['connected_account_id'] ) && $membership_settings['connected_account_id'] !== '';
1586}
1587
1588/**
1589 * Determines if any paid membership plan exists.
1590 *
1591 * @return bool Whether paid plan exists.
1592 */
1593function wpcom_launchpad_has_paid_membership_plans() {
1594    $membership_settings = wpcom_launchpad_get_membership_settings();
1595    if ( ! $membership_settings ) {
1596        return false;
1597    }
1598    return isset( $membership_settings['products'] ) && is_array( $membership_settings['products'] ) && ( count( $membership_settings['products'] ) > 0 );
1599}
1600
1601/**
1602 * Get membership settings.
1603 *
1604 * @return array|null Membership settings or null.
1605 */
1606function wpcom_launchpad_get_membership_settings() {
1607    $is_atomic = defined( 'IS_ATOMIC' ) && IS_ATOMIC;
1608
1609    // Memberships lib is only available on Simple sites.
1610    // A follow up will fetch membership settings for Atomic.
1611    if ( $is_atomic ) {
1612        return null;
1613    }
1614
1615    require_lib( 'memberships' );
1616    $blog_id  = get_current_blog_id();
1617    $settings = (array) get_memberships_settings_for_site( $blog_id );
1618    return $settings;
1619}
1620
1621/**
1622 * Callback for completing first post published task.
1623 *
1624 * @return void
1625 */
1626function wpcom_launchpad_track_publish_first_post_task() {
1627    // Ensure that Headstart posts don't mark this as complete.
1628    if ( defined( 'HEADSTART' ) && HEADSTART ) {
1629        return;
1630    }
1631    // Ensure that Imports don't mark this as complete.
1632    if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
1633        return;
1634    }
1635    // Since we share the same callback for generic first post and newsletter-specific, we mark both.
1636    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'first_post_published' );
1637    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'first_post_published_newsletter' );
1638}
1639
1640/**
1641 * Callback for completing migrate content task.
1642 *
1643 * @return void
1644 */
1645function wpcom_launchpad_track_migrate_content_task() {
1646    // Ensure that Headstart posts don't mark this as complete.
1647    // Headstart also enables WP_IMPORTING, so it is necessary to check both.
1648    if ( defined( 'HEADSTART' ) && HEADSTART ) {
1649        return;
1650    }
1651    // Only mark this complete when importing content.
1652    if ( ! defined( 'WP_IMPORTING' ) || ! WP_IMPORTING ) {
1653        return;
1654    }
1655    // Check the option to prevent setting this repeatedly during imports, which could spam tracks
1656    // and run extra unnecessary logic.
1657    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'migrate_content' ) ) ) {
1658        return;
1659    }
1660    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'migrate_content' );
1661}
1662
1663/**
1664 * Gets the number of published posts that were not created by Headstart.
1665 *
1666 * @return int
1667 */
1668function wpcom_launchpad_get_published_non_headstart_posts_count() {
1669    // Ensure that Headstart posts don't affect the count.
1670    if ( defined( 'HEADSTART' ) && HEADSTART ) {
1671        return 0;
1672    }
1673
1674    // Make sure we don't count any published posts that were created by Headstart.
1675    $headstart_posts_filter = array(
1676        'post_type'    => 'post',
1677        'fields'       => 'ids',
1678        'status'       => 'publish',
1679        'meta_key'     => '_hs_old_id',
1680        'meta_compare' => 'EXISTS',
1681    );
1682
1683    $total_posts_count     = wp_count_posts( 'post' )->publish;
1684    $headstart_posts_count = count( get_posts( $headstart_posts_filter ) );
1685
1686    return $total_posts_count - $headstart_posts_count;
1687}
1688
1689/**
1690 * Callback for completing the Write 3 posts task.
1691 *
1692 * @return void
1693 */
1694function wpcom_launchpad_track_write_3_posts_task() {
1695    // If the task is already completed, don't do anything.
1696    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'write_3_posts' ) ) ) {
1697        return;
1698    }
1699
1700    $published_non_headstart_posts = wpcom_launchpad_get_published_non_headstart_posts_count();
1701
1702    if ( $published_non_headstart_posts >= 3 ) {
1703        wpcom_mark_launchpad_task_complete( 'write_3_posts' );
1704    }
1705}
1706add_action( 'publish_post', 'wpcom_launchpad_track_write_3_posts_task' );
1707
1708/**
1709 * Callback for getting the number of posts published.
1710 *
1711 * @param Task $task The Task definition.
1712 * @return int
1713 */
1714function wpcom_launchpad_get_write_3_posts_repetition_count( $task ) {
1715    $published_non_headstart_posts = wpcom_launchpad_get_published_non_headstart_posts_count();
1716
1717    return min( $task['target_repetitions'], $published_non_headstart_posts );
1718}
1719
1720/**
1721 * Resolve the Newsletter Settings page URL, falling back to the canonical wp-admin path
1722 * if the Newsletter package isn't autoloaded (e.g. if the host plugin has been unloaded).
1723 *
1724 * @return string
1725 */
1726function wpcom_launchpad_get_newsletter_settings_url() {
1727    if ( class_exists( '\Automattic\Jetpack\Newsletter\Urls' ) ) {
1728        // @phan-suppress-next-line PhanUndeclaredClassMethod -- class_exists guarded above; provided by sibling autoloader.
1729        return \Automattic\Jetpack\Newsletter\Urls::get_newsletter_settings_url();
1730    }
1731
1732    return admin_url( 'admin.php?page=jetpack-newsletter&p=' . rawurlencode( '/?tab=settings' ) );
1733}
1734
1735/**
1736 * Returns the option value for a task and false if no option exists.
1737 *
1738 * @param Task $task The task data.
1739 * @return bool True if the option for the task is marked as complete, false otherwise.
1740 */
1741function wpcom_launchpad_is_task_option_completed( $task ) {
1742    if ( 'design_completed' === $task['id'] && wpcom_launchpad_has_site_been_created_through_onboarding_flow() ) {
1743        return true;
1744    }
1745
1746    $checklist = get_option( 'launchpad_checklist_tasks_statuses', array() );
1747    if ( ! empty( $checklist[ $task['id'] ] ) ) {
1748        return true;
1749    }
1750    if ( isset( $task['id_map'] ) && ! empty( $checklist[ $task['id_map'] ] ) ) {
1751        return true;
1752    }
1753    return false;
1754}
1755
1756/**
1757 * Helper function to detect whether we've reached the number of repetitions for a task, or if
1758 * the task has already been marked as complete.
1759 * This is most useful for cases where it's simpler to look at the cached action count than
1760 * injecting additional logic into complex code paths.
1761 * NOTE: This function should only be used when (re)computing the repetition count is quick.
1762 *
1763 * @param Task $task              The task data.
1764 * @param bool $is_option_complete Whether the underlying option has already been marked as complete.
1765 * @return bool True if the underlying option has been marked as complete, or if we detect that
1766 * target_repetitions has been reached.
1767 */
1768function wpcom_launchpad_is_repeated_task_complete( $task, $is_option_complete ) {
1769    if ( $is_option_complete ) {
1770        return true;
1771    }
1772
1773    if ( ! isset( $task['target_repetitions'] ) || ! is_int( $task['target_repetitions'] ) ) {
1774        return false;
1775    }
1776
1777    if ( ! isset( $task['repetition_count_callback'] ) || ! is_callable( $task['repetition_count_callback'] ) ) {
1778        return false;
1779    }
1780
1781    try {
1782        $repetition_count = call_user_func( $task['repetition_count_callback'], $task, 0 );
1783    } catch ( Exception $exception ) {
1784        return false;
1785    }
1786
1787    if ( ! is_int( $repetition_count ) ) {
1788        return false;
1789    }
1790
1791    $is_complete = $repetition_count >= $task['target_repetitions'];
1792
1793    if ( $is_complete ) {
1794        // If we detect the task has been completed, make sure we mark it as complete.
1795        wpcom_mark_launchpad_task_complete( $task['id'] );
1796    }
1797
1798    return $is_complete;
1799}
1800
1801/**
1802 * Returns the subtitle for the plan selected task
1803 *
1804 * @return string Subtitle text
1805 */
1806function wpcom_launchpad_get_plan_selected_subtitle() {
1807    if ( ! function_exists( 'wpcom_global_styles_in_use' ) || ! function_exists( 'wpcom_should_limit_global_styles' ) ) {
1808        return '';
1809    }
1810
1811    return wpcom_global_styles_in_use() && wpcom_should_limit_global_styles()
1812        ? __(
1813            'Your site contains premium styles. Upgrade now to publish them and unlock tons of other features.',
1814            'jetpack-mu-wpcom'
1815        ) : '';
1816}
1817
1818/**
1819 * Returns the badge text for the plan selected task
1820 *
1821 * @return string Badge text
1822 */
1823function wpcom_launchpad_get_plan_selected_badge_text() {
1824    if ( ! function_exists( 'wpcom_global_styles_in_use' ) || ! function_exists( 'wpcom_should_limit_global_styles' ) ) {
1825        return '';
1826    }
1827
1828    return wpcom_global_styles_in_use() && wpcom_should_limit_global_styles() ? __( 'Upgrade plan', 'jetpack-mu-wpcom' ) : '';
1829}
1830
1831/**
1832 * Callback for completing site launched task.
1833 *
1834 * Also included in bin/tests/isolated/suites/Guides/observer-modules/OnboardingUseCaseBlogTest.php
1835 *
1836 * @return void
1837 */
1838function wpcom_track_site_launch_task() {
1839    // it would be ideal if the registry was smart enough to map based on id_map but it isn't.
1840    // So we mark them all. We'd avoid this if we had dedicated callbacks for each task.
1841    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'site_launched' );
1842    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'link_in_bio_launched' );
1843    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'videopress_launched' );
1844    wpcom_launchpad_mark_launchpad_task_complete_if_active( 'blog_launched' );
1845
1846    // Remove site intent for the blog onboarding flow and disable launchpad.
1847    $site_intent = get_option( 'site_intent' );
1848    if ( 'design-first' === $site_intent ) {
1849        update_option( 'site_intent', '' );
1850        update_option( 'launchpad_screen', 'off' );
1851    }
1852
1853    // While in the design_first flow, if the user creates a post, deletes the default hello-world.
1854    $first_post_published = wpcom_launchpad_checklists()->is_task_id_complete( 'first_post_published' );
1855    if ( in_array( $site_intent, array( 'design-first' ), true ) && $first_post_published ) {
1856        $posts = get_posts( array( 'name' => 'hello-world' ) );
1857
1858        if ( count( $posts ) > 0 ) {
1859            wp_delete_post( $posts[0]->ID, true );
1860        }
1861    }
1862}
1863
1864/**
1865 * Mark the plugins reviewed task as complete.
1866 *
1867 * @return void
1868 */
1869function wpcom_launchpad_mark_review_plugins_complete() {
1870    wpcom_mark_launchpad_task_complete( 'review_plugins' );
1871}
1872/**
1873 * Callback that conditionally adds the site launch listener based on platform.
1874 *
1875 * @return void
1876 */
1877function wpcom_launchpad_add_site_launch_listener() {
1878    if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1879        add_action( 'wpcom_site_launched', 'wpcom_track_site_launch_task' );
1880    } else {
1881        add_action( 'update_option_blog_public', 'wpcom_launchpad_launch_task_listener_atomic', 10, 2 );
1882    }
1883}
1884
1885/**
1886 * Callback that fires when `blog_public` is updated.
1887 *
1888 * @param string $old_value The updated option value.
1889 * @param string $new_value The previous option value.
1890 * @return void
1891 */
1892function wpcom_launchpad_launch_task_listener_atomic( $old_value, $new_value ) {
1893    $blog_public = (int) $new_value;
1894    // 'blog_public' is set to '1' when a site is launched.
1895    if ( $blog_public === 1 ) {
1896        wpcom_track_site_launch_task();
1897    }
1898}
1899
1900/**
1901 * Callback for email verification completion.
1902 *
1903 * @return bool True if email is verified, false otherwise.
1904 */
1905function wpcom_launchpad_is_email_verified() {
1906    // TODO: handle the edge case where an Atomic user can be unverified.
1907    if ( ! class_exists( 'Email_Verification' ) ) {
1908        return true;
1909    }
1910
1911    return ! Email_Verification::is_email_unverified();
1912}
1913
1914/**
1915 * Handles WPCOM action fired when email verification is completed.
1916 *
1917 * @param int $user_id The user ID.
1918 */
1919function wpcom_launchpad_mark_verify_email_complete( $user_id ) {
1920    $user = get_user_by( 'id', $user_id );
1921    if ( empty( $user ) ) {
1922        return;
1923    }
1924
1925    // Completed task events usually have a `goals` prop, however verifying email isn't associated
1926    // with a specific site, so can't be associated with site goals.
1927    $extra_props = array();
1928
1929    wpcom_launchpad_track_completed_task( 'verify_email', $extra_props, $user );
1930}
1931add_action( 'wpcom_email_verification_complete', 'wpcom_launchpad_mark_verify_email_complete' );
1932
1933/**
1934 * Callback to determine email verification visibility.
1935 * This uses a heuristic to determine whether it's likely that the user needed to
1936 * verify their email at the time the site was created. That way we're not showing
1937 * the completed task to users who were verified before creating this site.
1938 *
1939 * @return bool True when the email verification task should be visible.
1940 */
1941function wpcom_launchpad_is_email_task_visible() {
1942    if ( ! class_exists( 'Email_Verification' ) ) {
1943        // Don't show the task if we don't have the ability to complete it
1944        return false;
1945    }
1946
1947    if ( Email_Verification::is_email_unverified() ) {
1948        return true;
1949    }
1950
1951    if ( ! function_exists( 'get_blog_count_for_user' ) ) {
1952        return true;
1953    }
1954
1955    return get_blog_count_for_user() < 2;
1956}
1957
1958/**
1959 * If the site has a paid-subscriber goal.
1960 *
1961 * @return bool True if the site has a paid-subscriber goal, false otherwise.
1962 */
1963function wpcom_launchpad_has_goal_paid_subscribers() {
1964    return in_array( 'paid-subscribers', get_option( 'site_goals', array() ), true );
1965}
1966
1967/**
1968 * If the site has a import-subscriber goal.
1969 *
1970 * @return bool True if the site has a import-subscriber goal, false otherwise.
1971 */
1972function wpcom_launchpad_has_goal_import_subscribers() {
1973    return in_array( 'import-subscribers', get_option( 'site_goals', array() ), true );
1974}
1975
1976/**
1977 * Determines whether or not the link-in-bio launch task is enabled
1978 *
1979 * @return boolean True if link-in-bio launch task is enabled
1980 */
1981function wpcom_launchpad_is_link_in_bio_launch_disabled() {
1982    return ! wpcom_is_checklist_task_complete( 'links_added' );
1983}
1984
1985/**
1986 * Determines whether or not the videopress launch task is enabled
1987 *
1988 * @return boolean True if videopress launch task is enabled
1989 */
1990function wpcom_launchpad_is_videopress_launch_disabled() {
1991    return ! wpcom_is_checklist_task_complete( 'videopress_upload' );
1992}
1993
1994/**
1995 * Determines whether or not the blog launch task is enabled
1996 *
1997 * @return boolean True if blog launch task is enabled
1998 */
1999function wpcom_launchpad_is_blog_launched_task_disabled() {
2000    if ( 'design-first' === get_option( 'site_intent' ) ) {
2001        // We only want the blog_launched task enabled after other key tasks are all complete.
2002        if ( wpcom_is_checklist_task_complete( 'plan_completed' )
2003            && wpcom_is_checklist_task_complete( 'domain_upsell' )
2004            && wpcom_is_checklist_task_complete( 'setup_blog' ) ) {
2005            return false;
2006        }
2007        return true;
2008    }
2009    return false;
2010}
2011
2012/**
2013 * Determines whether or not the videopress upload task is enabled
2014 *
2015 * @return boolean True if videopress upload task is enabled
2016 */
2017function wpcom_launchpad_is_videopress_upload_disabled() {
2018    return wpcom_is_checklist_task_complete( 'videopress_upload' );
2019}
2020
2021/**
2022 * Update Launchpad's video_uploaded task.
2023 *
2024 * Only updated for videopress flows currently.
2025 *
2026 * @param string $post_id The id of the post being udpated.
2027 * @return void
2028 */
2029function wpcom_launchpad_track_video_uploaded_task( $post_id ) {
2030    // Not using `wp_attachment_is` because it requires the actual file
2031    // which is not the case for Atomic VideoPress.
2032    if ( ! str_starts_with( get_post_mime_type( $post_id ), 'video/' ) ) {
2033        return;
2034    }
2035    wpcom_mark_launchpad_task_complete( 'videopress_upload' );
2036}
2037
2038/**
2039 * Mark the site_title task as complete if the site title is not empty and not the default.
2040 *
2041 * @param string $old_value The old value of the site title.
2042 * @param string $value The new value of the site title.
2043 *
2044 * @return void
2045 */
2046function wpcom_launchpad_mark_site_title_complete( $old_value, $value ) {
2047    if ( $value !== $old_value ) {
2048        wpcom_mark_launchpad_task_complete( 'site_title' );
2049    }
2050}
2051add_action( 'update_option_blogname', 'wpcom_launchpad_mark_site_title_complete', 10, 3 );
2052
2053/**
2054 * Mark the setup site task as complete if the value is changed.
2055 *
2056 * @param string $old_value The old value of the site title.
2057 * @param string $value The new value of the site title.
2058 *
2059 * @return void
2060 */
2061function wpcom_launchpad_mark_setup_site_tasks_complete( $old_value, $value ) {
2062    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2063        return;
2064    }
2065
2066    if ( wp_installing() ) {
2067        return;
2068    }
2069
2070    if ( $value !== $old_value ) {
2071        wpcom_mark_launchpad_task_complete( 'setup_free' );
2072    }
2073}
2074add_action( 'update_option_blogname', 'wpcom_launchpad_mark_setup_site_tasks_complete', 10, 3 );
2075add_action( 'update_option_blogdescription', 'wpcom_launchpad_mark_setup_site_tasks_complete', 10, 3 );
2076add_action( 'update_option_site_icon', 'wpcom_launchpad_mark_setup_site_tasks_complete', 10, 3 );
2077add_action( 'update_option_site_logo', 'wpcom_launchpad_mark_setup_site_tasks_complete', 10, 3 );
2078
2079/**
2080 * Mark the setup_general task as complete if the site title or the site description is changed.
2081 *
2082 * @param string $old_value The old value of the site title.
2083 * @param string $value The new value of the site title.
2084 *
2085 * @return void
2086 */
2087function wpcom_launchpad_mark_setup_general_task_complete( $old_value, $value ) {
2088    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2089        return;
2090    }
2091
2092    if ( wp_installing() ) {
2093        return;
2094    }
2095
2096    if ( $old_value !== $value ) {
2097        wpcom_mark_launchpad_task_complete( 'setup_general' );
2098    }
2099}
2100add_action( 'update_option_blogname', 'wpcom_launchpad_mark_setup_general_task_complete', 10, 3 );
2101add_action( 'update_option_blogdescription', 'wpcom_launchpad_mark_setup_general_task_complete', 10, 3 );
2102
2103/**
2104 * Mark the enable_subscribers_modal task complete
2105 * if its option is updated to `true`.
2106 *
2107 * @param string $old_value The old value of the option.
2108 * @param string $value The new value of the option.
2109 *
2110 * @return void
2111 */
2112function wpcom_launchpad_mark_enable_subscribers_modal_complete( $old_value, $value ) {
2113    if ( $value ) {
2114        wpcom_mark_launchpad_task_complete( 'enable_subscribers_modal' );
2115    }
2116}
2117add_action( 'update_option_sm_enabled', 'wpcom_launchpad_mark_enable_subscribers_modal_complete', 10, 3 );
2118add_action( 'add_option_sm_enabled', 'wpcom_launchpad_mark_enable_subscribers_modal_complete', 10, 3 );
2119
2120/**
2121 * Mark the Sensei setup task as complete if the value is changed.
2122 *
2123 * @param string $old_value The old value of the option.
2124 * @param string $value The new value of the option.
2125 *
2126 * @return void
2127 */
2128function wpcom_launchpad_mark_sensei_setup_complete( $old_value, $value ) {
2129    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2130        return;
2131    }
2132
2133    if ( wp_installing() ) {
2134        return;
2135    }
2136
2137    if ( true === $value ) {
2138        wpcom_mark_launchpad_task_complete( 'sensei_setup' );
2139    }
2140}
2141add_action( 'update_option_sensei_home_tasks_list_is_completed', 'wpcom_launchpad_mark_sensei_setup_complete', 10, 3 );
2142add_action( 'add_option_sensei_home_tasks_list_is_completed', 'wpcom_launchpad_mark_sensei_setup_complete', 10, 3 );
2143
2144/**
2145 * Determine `domain_claim` task visibility.
2146 *
2147 * @return bool True if we should show the task, false otherwise.
2148 */
2149function wpcom_launchpad_domain_claim_is_visible_callback() {
2150    if ( ! function_exists( 'wpcom_site_has_feature' ) ) {
2151        return false;
2152    }
2153
2154    return wpcom_site_has_feature( 'custom-domain' );
2155}
2156
2157/**
2158 * Determines whether or not domain claim task is completed.
2159 *
2160 * @return bool True if domain claim task is completed.
2161 */
2162function wpcom_launchpad_is_domain_claim_completed() {
2163    if ( ! function_exists( 'wpcom_get_site_purchases' ) ) {
2164        return false;
2165    }
2166
2167    $site_purchases = wpcom_get_site_purchases();
2168
2169    // Check if the site has any domain purchases.
2170    $domain_purchases = array_filter(
2171        $site_purchases,
2172        function ( $site_purchase ) {
2173            return in_array( $site_purchase->product_type, array( 'domain_map', 'domain_reg' ), true );
2174        }
2175    );
2176
2177    return ! empty( $domain_purchases );
2178}
2179
2180/**
2181 * When a new page is added to the site, mark the add_new_page task complete as needed.
2182 *
2183 * @param int     $post_id The ID of the post being updated.
2184 * @param ?object $post    The post object.
2185 */
2186function wpcom_launchpad_add_new_page_check( $post_id, $post = null ) {
2187    // There are cases when plugins trigger insert hooks without arguments, we just return in this case.
2188    if ( null === $post ) {
2189        return;
2190    }
2191
2192    // Don't do anything if the task is already complete.
2193    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'add_new_page' ) ) ) {
2194        return;
2195    }
2196
2197    // We only care about pages, ignore other post types.
2198    if ( $post->post_type !== 'page' ) {
2199        return;
2200    }
2201
2202    // Ensure that Headstart posts don't mark this as complete
2203    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2204        return;
2205    }
2206
2207    // We only care about published pages. Pages added via the API are not published by default.
2208    if ( $post->post_status !== 'publish' ) {
2209        return;
2210    }
2211
2212    // This is necessary to avoid marking the task as complete when the about page is updated.
2213    if ( $post_id === wpcom_launchpad_get_site_about_page_id() ) {
2214        return;
2215    }
2216
2217    wpcom_mark_launchpad_task_complete( 'add_new_page' );
2218}
2219add_action( 'wp_insert_post', 'wpcom_launchpad_add_new_page_check', 10, 3 );
2220
2221/**
2222 * Return the about page id, if any.
2223 *
2224 * This function will retrieve the page from the cache whenever possible.
2225 *
2226 * @return int|null The page ID of the 'About' page if it exists, null otherwise.
2227 */
2228function wpcom_launchpad_get_site_about_page_id() {
2229    // First, attempt to get the page ID from the cache.
2230    $about_page_id_cache_key = 'wpcom_about_page_id';
2231    $about_page_id           = wp_cache_get( $about_page_id_cache_key, 'jetpack_mu_wpcom' );
2232
2233    if ( false !== $about_page_id ) {
2234        $about_page_id = (int) $about_page_id;
2235        if ( $about_page_id > 0 ) {
2236            return $about_page_id;
2237        }
2238
2239        return null;
2240    }
2241
2242    // Call the current function implementation without any caching logic.
2243    $about_page_id = wpcom_launchpad_find_site_about_page_id();
2244
2245    if ( null === $about_page_id ) {
2246        // In the event that we don't have a page id, cache -1 to avoid caching null
2247        $value_to_cache = -1;
2248    } else {
2249        $value_to_cache = $about_page_id;
2250    }
2251
2252    // Use wp_cache_add() to avoid cache stampedes, and specify an expiration time
2253    wp_cache_add( $about_page_id_cache_key, $value_to_cache, 'jetpack_mu_wpcom', 3600 );
2254
2255    return $about_page_id;
2256}
2257
2258/**
2259 * Determine if the site has an 'About' page.
2260 * We do this by loading the `en` annotation for the site and theme.
2261 * We then check if there is a post with the title 'About'. If there is, we get the `hs_old_id` and check to make sure a corresponding post exists in the user's locale.
2262 *
2263 * @return int|null The page ID of the 'About' page if it exists, null otherwise.
2264 */
2265function wpcom_launchpad_find_site_about_page_id() {
2266    if ( ! function_exists( 'wpcom_get_theme_annotation' ) ) {
2267        return null;
2268    }
2269
2270    $annotation = wpcom_get_theme_annotation( get_stylesheet() );
2271
2272    // Return null if there is no annotation, an error, or the annotation doesn't have any content.
2273    if ( ! $annotation || ! is_array( $annotation ) || ! isset( $annotation['content'] ) || ! is_array( $annotation['content'] ) ) {
2274        return null;
2275    }
2276
2277    // Use the annotation to build up a list of 'About' pages.
2278    $headstart_about_pages = array_filter(
2279        $annotation['content'],
2280        function ( $page ) {
2281            if ( isset( $page['post_type'] ) && 'page' !== $page['post_type'] ) {
2282                return false;
2283            }
2284
2285            if ( isset( $page['post_name'] ) && 'about' === $page['post_name'] ) {
2286                return true;
2287            }
2288            if ( isset( $page['post_title'] ) && str_contains( $page['post_title'], 'About' ) ) {
2289                return true;
2290            }
2291        }
2292    );
2293
2294    // Return null if there are no 'About' pages.
2295    if ( empty( $headstart_about_pages ) ) {
2296        return null;
2297    }
2298
2299    // Get the hs_old_ids for the 'About' pages. We'll use these to find any published pages with the same hs_old_id.
2300    $headstart_about_page_hs_old_ids = array_map(
2301        function ( $about_page ) {
2302            return $about_page['hs_old_id'];
2303        },
2304        $headstart_about_pages
2305    );
2306
2307    // Return null if there aren't any hs_old_ids to check.
2308    if ( empty( $headstart_about_page_hs_old_ids ) ) {
2309        return null;
2310    }
2311
2312    $filters     = array(
2313        'post_type'    => 'page',
2314        'numberposts'  => 1,
2315        'fields'       => 'ids',
2316        'orderby'      => 'meta_value',
2317        'meta_key'     => '_hs_old_id',
2318        'meta_compare' => 'IN',
2319        'meta_value'   => $headstart_about_page_hs_old_ids,
2320    );
2321    $about_pages = get_posts( $filters );
2322
2323    // Return null if we couldn't find any pages matching the hs_old_ids.
2324    if ( empty( $about_pages ) ) {
2325        return null;
2326    }
2327
2328    // Return the id of the first About page.
2329    return (int) $about_pages[0];
2330}
2331
2332/**
2333 * Check to see if a string has been translated.
2334 *
2335 * @param string $string The string to check.
2336 * @param string $domain The text domain to use.
2337 * @return bool True if the string has been translated, false otherwise.
2338 */
2339function wpcom_launchpad_has_translation( $string, $domain = 'jetpack-mu-wpcom' ) {
2340    if ( empty( $string ) ) {
2341        return false;
2342    }
2343
2344    $current_locale = get_user_locale();
2345    if ( is_string( $current_locale ) && str_starts_with( $current_locale, 'en' ) ) {
2346        return true;
2347    }
2348
2349    // phpcs:disable WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralDomain
2350    $translated_string = __( $string, $domain );
2351
2352    return $translated_string !== $string;
2353}
2354
2355/**
2356 * Determine `add_new_page` task visibility. The task is visible if there is no 'About' page on the site.
2357 *
2358 * @return bool True if we should show the task, false otherwise.
2359 */
2360function wpcom_launchpad_is_add_about_page_visible() {
2361    return ! wpcom_launchpad_is_update_about_page_task_visible() && registered_meta_key_exists( 'post', '_wpcom_template_layout_category', 'page' );
2362}
2363
2364/**
2365 * Determine `front_page_updated` task visibility.
2366 *
2367 * @return bool True if we should show the task, false otherwise.
2368 */
2369function wpcom_launchpad_is_front_page_updated_visible() {
2370    $show_on_front = get_option( 'show_on_front' );
2371    $blog_on_front = $show_on_front === 'posts' || ( $show_on_front === 'page' && get_option( 'page_on_front' ) === '0' );
2372
2373    if ( $blog_on_front && ! wp_is_block_theme() ) {
2374        return false;
2375    }
2376
2377    return true;
2378}
2379
2380/**
2381 * Determine `site_title` task visibility. The task is not visible if the name was already set.
2382 *
2383 * @return bool True if we should show the task, false otherwise.
2384 */
2385function wpcom_launchpad_is_site_title_task_visible() {
2386    // Hide the task if it's already completed on write intent
2387    if (
2388        ( 'launched' === get_option( 'launch-status' ) ) &&
2389        get_option( 'site_intent' ) === 'write' &&
2390        wpcom_launchpad_is_task_option_completed( array( 'id' => 'site_title' ) )
2391    ) {
2392        return false;
2393    }
2394    return true;
2395}
2396/**
2397 * Completion hook for the `add_about_page` task.
2398 *
2399 * @param int     $post_id The post ID.
2400 * @param ?object $post    The post object.
2401 */
2402function wpcom_launchpad_add_about_page_check( $post_id, $post = null ) {
2403    // There are cases when plugins trigger insert hooks without arguments, we just return in this case.
2404    if ( null === $post ) {
2405        return;
2406    }
2407
2408    // Ensure that Headstart posts don't mark this as complete
2409    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2410        return;
2411    }
2412
2413    // We only care about pages, ignore other post types.
2414    if ( $post->post_type !== 'page' ) {
2415        return;
2416    }
2417
2418    // We only care about published pages. Pages added via the API are not published by default.
2419    if ( $post->post_status !== 'publish' ) {
2420        return;
2421    }
2422
2423    // Don't do anything if we don't have the page category post_meta field registered.
2424    if ( ! registered_meta_key_exists( 'post', '_wpcom_template_layout_category', 'page' ) ) {
2425        return;
2426    }
2427
2428    // Don't do anything if the task is already complete.
2429    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'add_about_page' ) ) ) {
2430        return;
2431    }
2432
2433    // If the page is the previously located About page, ignore it.
2434    $about_page_id = wpcom_launchpad_get_site_about_page_id();
2435    if ( null !== $about_page_id && $post->ID === $about_page_id ) {
2436        return;
2437    }
2438
2439    $stored_page_categories = get_post_meta( $post->ID, '_wpcom_template_layout_category', true );
2440    if ( ! is_array( $stored_page_categories ) || ! in_array( 'about', $stored_page_categories, true ) ) {
2441        return;
2442    }
2443
2444    wpcom_mark_launchpad_task_complete( 'add_about_page' );
2445}
2446add_action( 'wp_insert_post', 'wpcom_launchpad_add_about_page_check', 10, 3 );
2447
2448/**
2449 * Completion hook for the `front_page_updated` task.
2450 *
2451 * @param int     $post_id The post ID.
2452 * @param ?object $post    The post object.
2453 * @return void
2454 */
2455function wpcom_launchpad_front_page_updated_check( $post_id, $post = null ) {
2456    // There are cases when plugins trigger insert hooks without arguments, we just return in this case.
2457    if ( null === $post ) {
2458        return;
2459    }
2460
2461    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2462        return;
2463    }
2464
2465    // We only care about pages, ignore other post types.
2466    if ( $post->post_type !== 'page' ) {
2467        return;
2468    }
2469
2470    // Don't do anything if the task is already complete.
2471    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'front_page_updated' ) ) ) {
2472        return;
2473    }
2474
2475    // We only complete the task if the page is the front page.
2476    $front_page_id = (int) get_option( 'page_on_front' );
2477    if ( $post_id !== $front_page_id ) {
2478        return;
2479    }
2480
2481    wpcom_mark_launchpad_task_complete( 'front_page_updated' );
2482}
2483add_action( 'wp_insert_post', 'wpcom_launchpad_front_page_updated_check', 10, 3 );
2484
2485/**
2486 * We also need to complete the `front_page_updated` task when the front page option is updated.
2487 *
2488 * @return void
2489 */
2490function wpcom_launchpad_front_page_updated_option_check() {
2491    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2492        return;
2493    }
2494
2495    // Don't do anything if the task is already complete.
2496    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'front_page_updated' ) ) ) {
2497        return;
2498    }
2499
2500    wpcom_mark_launchpad_task_complete( 'front_page_updated' );
2501}
2502add_action( 'update_option_page_on_front', 'wpcom_launchpad_front_page_updated_option_check', 10, 3 );
2503add_action( 'add_option_page_on_front', 'wpcom_launchpad_front_page_updated_option_check', 10, 3 );
2504
2505/**
2506 * Determine `update_about_page` task visibility. The task is visible if there is an 'About' page on the site.
2507 *
2508 * @return bool True if we should show the task, false otherwise.
2509 */
2510function wpcom_launchpad_is_update_about_page_task_visible() {
2511    // The task isn't visible if the task title hasn't been translated into the current locale.
2512    if ( ! wpcom_launchpad_has_translation( 'Update your About page', 'jetpack-mu-wpcom' ) ) {
2513        return false;
2514    }
2515
2516    return wpcom_launchpad_get_site_about_page_id() !== null;
2517}
2518
2519/**
2520 * When a page is updated, check to see if it's the About page and mark the task complete accordingly.
2521 *
2522 * @param int     $post_id The ID of the post being updated.
2523 * @param WP_Post $post The post object.
2524 */
2525function wpcom_launchpad_update_about_page_check( $post_id, $post ) {
2526    // Ensure that Headstart posts don't mark this as complete
2527    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2528        return;
2529    }
2530
2531    // We only care about pages, ignore other post types.
2532    if ( $post->post_type !== 'page' ) {
2533        return;
2534    }
2535
2536    // We only care about published pages. Pages added via the API are not published by default.
2537    if ( $post->post_status !== 'publish' ) {
2538        return;
2539    }
2540
2541    // Don't do anything if the task is already complete.
2542    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'update_about_page' ) ) ) {
2543        return;
2544    }
2545
2546    // If the page is not the previously located About page, ignore it.
2547    $about_page_id = wpcom_launchpad_get_site_about_page_id();
2548    if ( null === $about_page_id || $post->ID !== $about_page_id ) {
2549        return;
2550    }
2551
2552    wpcom_mark_launchpad_task_complete( 'update_about_page' );
2553}
2554add_action( 'post_updated', 'wpcom_launchpad_update_about_page_check', 10, 3 );
2555
2556/**
2557 * Determine `edit_page` task visibility. The task is visible if there is at least one page and the add_new_page task is complete.
2558 *
2559 * @return bool True if we should show the task, false otherwise.
2560 */
2561function wpcom_launchpad_is_edit_page_task_visible() {
2562    // Don't show the task if the update_about_page task is visible.
2563    if ( wpcom_launchpad_is_update_about_page_task_visible() ) {
2564        return false;
2565    }
2566
2567    // Otherwise, don't show the task until after a page has been added.
2568    if ( ! wpcom_launchpad_is_task_option_completed( array( 'id' => 'add_new_page' ) ) ) {
2569        return false;
2570    }
2571
2572    // Show the task if there is at least one page.
2573    return ! empty(
2574        get_posts(
2575            array(
2576                'numberposts' => 1,
2577                'post_type'   => 'page',
2578            )
2579        )
2580    );
2581}
2582
2583/**
2584 * Mark the customize_welcome_message task complete
2585 * if the subscription_options['welcome'] value
2586 * for the welcome message has changed on option update.
2587 *
2588 * @param mixed $old_value The old value of the welcome message.
2589 * @param mixed $value The new value of the welcome message.
2590 *
2591 * @return void
2592 */
2593function wpcom_launchpad_mark_customize_welcome_message_complete_on_update( $old_value, $value ) {
2594    $new_welcome = is_array( $value ) && isset( $value['welcome'] ) ? $value['welcome'] : '';
2595    $old_welcome = is_array( $old_value ) && isset( $old_value['welcome'] ) ? $old_value['welcome'] : '';
2596    if ( $new_welcome !== $old_welcome ) {
2597        wpcom_mark_launchpad_task_complete( 'customize_welcome_message' );
2598    }
2599}
2600add_action( 'update_option_subscription_options', 'wpcom_launchpad_mark_customize_welcome_message_complete_on_update', 10, 2 );
2601
2602/**
2603 * Mark the customize_welcome_message task complete
2604 * if the subscription_options['welcome'] value
2605 * for the welcome message has been added.
2606 *
2607 * @param mixed $value The value of the welcome message.
2608 *
2609 * @return void
2610 */
2611function wpcom_launchpad_mark_customize_welcome_message_complete_on_add( $value ) {
2612    if ( is_array( $value ) && $value['welcome'] ) {
2613        wpcom_mark_launchpad_task_complete( 'customize_welcome_message' );
2614    }
2615}
2616add_action( 'add_option_subscription_options', 'wpcom_launchpad_mark_customize_welcome_message_complete_on_add', 10, 1 );
2617
2618/**
2619 * Mark the WooCommerce setup task as complete the setup task list is in
2620 * the completed list or in the hidden list.
2621 *
2622 * @param array $old_value The old value of the option.
2623 * @param array $value The new value of the option.
2624 *
2625 * @return void
2626 */
2627function wpcom_launchpad_mark_woocommerce_setup_complete( $old_value, $value ) {
2628    if ( ! in_array( 'setup', $value, true ) ) {
2629        return;
2630    }
2631
2632    wpcom_mark_launchpad_task_complete( 'woocommerce_setup' );
2633}
2634add_action( 'update_option_woocommerce_task_list_completed_lists', 'wpcom_launchpad_mark_woocommerce_setup_complete', 10, 3 );
2635add_action( 'add_option_woocommerce_task_list_completed_lists', 'wpcom_launchpad_mark_woocommerce_setup_complete', 10, 3 );
2636add_action( 'update_option_woocommerce_task_list_hidden_lists', 'wpcom_launchpad_mark_woocommerce_setup_complete', 10, 3 );
2637add_action( 'add_option_woocommerce_task_list_hidden_lists', 'wpcom_launchpad_mark_woocommerce_setup_complete', 10, 3 );
2638
2639/**
2640 * When a page is updated, check to see if we've already completed the `add_new_page` task and mark the `edit_page` task complete accordingly.
2641 *
2642 * @param int     $post_id The ID of the post being updated.
2643 * @param WP_Post $post The post object.
2644 */
2645function wpcom_launchpad_edit_page_check( $post_id, $post ) {
2646    // Don't do anything if the task is already complete.
2647    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'edit_page' ) ) ) {
2648        return;
2649    }
2650
2651    // Don't do anything if the add_new_page task is incomplete.
2652    if ( ! wpcom_launchpad_is_task_option_completed( array( 'id' => 'add_new_page' ) ) ) {
2653        return false;
2654    }
2655
2656    // We only care about pages, ignore other post types.
2657    if ( $post->post_type !== 'page' ) {
2658        return;
2659    }
2660
2661    // Ensure that Headstart posts don't mark this as complete
2662    if ( defined( 'HEADSTART' ) && HEADSTART ) {
2663        return;
2664    }
2665
2666    // We only care about published pages. Pages added via the API are not published by default.
2667    if ( $post->post_status !== 'publish' ) {
2668        return;
2669    }
2670
2671    wpcom_mark_launchpad_task_complete( 'edit_page' );
2672}
2673add_action( 'post_updated', 'wpcom_launchpad_edit_page_check', 10, 3 );
2674
2675/**
2676 * Determine `add_subscribe_block` task visibility. The task is visible if using a FSE theme.
2677 *
2678 * @return bool True if we should show the task, false otherwise.
2679 */
2680function wpcom_launchpad_is_add_subscribe_block_visible() {
2681    return \Automattic\Jetpack\Blocks::is_fse_theme();
2682}
2683
2684/**
2685 * When a template or template part is saved, check if the subscribe block is in the content.
2686 *
2687 * @param int     $post_id The ID of the post being updated.
2688 * @param WP_Post $post The post object.
2689 *
2690 * @return bool True if the task is completed, false otherwise.
2691 */
2692function wpcom_launchpad_add_subscribe_block_check( $post_id, $post ) {
2693    // If this is just a revision, don't proceed.
2694    if ( wp_is_post_revision( $post_id ) ) {
2695        return;
2696    }
2697
2698    // Check if it's a published template or template part.
2699    if ( $post->post_status !== 'publish' || ( $post->post_type !== 'wp_template' && $post->post_type !== 'wp_template_part' ) ) {
2700        return;
2701    }
2702
2703    // Check if our subscribe block is in the template or template part content.
2704    if ( has_block( 'jetpack/subscriptions', $post->post_content ) ) {
2705        // Run your specific function if the subscribe block is found.
2706        wpcom_mark_launchpad_task_complete( 'add_subscribe_block' );
2707    }
2708}
2709
2710// Hook the function to the save_post action for all post types.
2711add_action( 'save_post', 'wpcom_launchpad_add_subscribe_block_check', 10, 2 );
2712
2713/**
2714 * Returns if the site has domain or bundle purchases.
2715 *
2716 * @return array Array of booleans, first value is if the site has a bundle, second is if the site has a domain.
2717 */
2718function wpcom_launchpad_domain_customize_check_purchases() {
2719    if ( ! function_exists( 'wpcom_get_site_purchases' ) ) {
2720        return false;
2721    }
2722
2723    $site_purchases = wpcom_get_site_purchases();
2724    $has_bundle     = false;
2725    $has_domain     = false;
2726
2727    foreach ( $site_purchases as $site_purchase ) {
2728        if ( 'bundle' === $site_purchase->product_type ) {
2729            $has_bundle = true;
2730        }
2731
2732        if ( in_array( $site_purchase->product_type, array( 'domain_map', 'domain_reg' ), true ) ) {
2733            $has_domain = true;
2734        }
2735    }
2736
2737    return array( $has_bundle, $has_domain );
2738}
2739
2740/**
2741 * Determines whether or not domain customize task is complete.
2742 *
2743 * @param Task  $task    The Task object.
2744 * @param mixed $default The default value.
2745 * @return bool True if domain customize task is complete.
2746 */
2747function wpcom_launchpad_is_domain_customize_completed( $task, $default ) {
2748    $result = wpcom_launchpad_domain_customize_check_purchases();
2749
2750    if ( false === $result ) {
2751        return false;
2752    }
2753
2754    $has_domain = $result[1];
2755
2756    // For users with a custom domain, show the task as complete.
2757    if ( $has_domain ) {
2758        return true;
2759    }
2760
2761    // For everyone else, show the task as incomplete.
2762    return $default;
2763}
2764
2765/**
2766 * Mark `domain_claim`, `domain_upsell`, and `domain_upsell_deferred` tasks complete
2767 * when a domain product is activated.
2768 *
2769 * @param int    $blog_id The blog ID.
2770 * @param int    $user_id The user ID.
2771 * @param string $product_id The product ID.
2772 *
2773 * @return void
2774 */
2775function wpcom_launchpad_mark_domain_tasks_complete( $blog_id, $user_id, $product_id ) {
2776    if ( ! class_exists( 'domains' ) ) {
2777        return;
2778    }
2779
2780    if ( ! domains::is_domain_product( $product_id ) ) {
2781        return;
2782    }
2783
2784    wpcom_mark_launchpad_task_complete( 'domain_claim' );
2785    wpcom_mark_launchpad_task_complete( 'domain_upsell' );
2786    wpcom_mark_launchpad_task_complete( 'domain_upsell_deferred' );
2787}
2788add_action( 'activate_product', 'wpcom_launchpad_mark_domain_tasks_complete', 10, 6 );
2789
2790/**
2791 * Mark `plan_selected`, and `plan_completed` tasks complete when a plan product is purchased.
2792 *
2793 * @param int $blog_id The blog ID.
2794 *
2795 * @return void
2796 */
2797function wpcom_launchpad_mark_plan_tasks_complete( $blog_id ) {
2798    require_once WP_CONTENT_DIR . '/admin-plugins/wpcom-billing.php';
2799    $current_plan = WPCOM_Store_API::get_current_plan( $blog_id );
2800
2801    if ( $current_plan['is_free'] ?? true ) {
2802        return;
2803    }
2804
2805    wpcom_mark_launchpad_task_complete( 'plan_selected' );
2806    wpcom_mark_launchpad_task_complete( 'plan_completed' );
2807}
2808add_action( 'activate_product', 'wpcom_launchpad_mark_plan_tasks_complete', 10, 6 );
2809
2810/**
2811 * Re-trigger email campaigns for blog onboarding after user edit one of the fields in the launchpad.
2812 *
2813 * Also included bin/tests/isolated/suites/Guides/observer-modules/OnboardingUseCaseBlogTest.php#601
2814 *
2815 * @return void
2816 */
2817function wpcom_trigger_email_campaign() {
2818    $site_intent = get_option( 'site_intent' );
2819    if ( ! $site_intent ) {
2820        return;
2821    }
2822
2823    if ( 'design-first' !== $site_intent ) {
2824        return;
2825    }
2826
2827    MarketingEmailCampaigns::start_tailored_use_case_new_site_workflows_if_eligible(
2828        get_current_user_id(),
2829        get_current_blog_id(),
2830        $site_intent
2831    );
2832}
2833add_action( 'update_option_launchpad_checklist_tasks_statuses', 'wpcom_trigger_email_campaign', 10, 3 );
2834
2835/**
2836 * Mark task complete when plugin is installed.
2837 */
2838function wpcom_launchpad_mark_plugin_installed_complete() {
2839    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'install_custom_plugin' ) ) ) {
2840        return;
2841    }
2842    wpcom_mark_launchpad_task_complete( 'install_custom_plugin' );
2843}
2844add_action( 'jetpack_plugin_installed', 'wpcom_launchpad_mark_plugin_installed_complete', 10 );
2845
2846/**
2847 * Mark task complete when theme is selected.
2848 *
2849 * @param array $new_theme    The new theme object.
2850 * @param array $old_theme The old theme object.
2851 */
2852function wpcom_launchpad_mark_theme_selected_complete( $new_theme, $old_theme ) {
2853    // This hook runs when site just gets setup, lets prevent checklist item from being complete
2854    // when the theme is the same.
2855    $is_same_theme = $new_theme['name'] === $old_theme['name'];
2856    if ( wpcom_launchpad_is_task_option_completed( array( 'id' => 'site_theme_selected' ) ) || $is_same_theme ) {
2857        return;
2858    }
2859    wpcom_mark_launchpad_task_complete( 'site_theme_selected' );
2860}
2861add_action( 'jetpack_sync_current_theme_support', 'wpcom_launchpad_mark_theme_selected_complete', 10, 2 );
2862
2863/**
2864 * Returns the latest draft post ID, otherwise false.
2865 * Similar to wp_get_recent_posts, except we only need the ID.
2866 *
2867 * The response is cached for the lifetime of the current request.
2868 *
2869 * @return int | boolean
2870 */
2871function wpcom_launchpad_get_latest_draft_id() {
2872    // The result is cached for the current request
2873    static $cached_blog_id  = null;
2874    static $cached_draft_id = null;
2875
2876    if ( $cached_blog_id === get_current_blog_id() && $cached_draft_id !== null ) {
2877        return $cached_draft_id;
2878    }
2879
2880    // Query for the latest draft post
2881    $args = array(
2882        'posts_per_page' => 1,
2883        'post_status'    => 'draft',
2884        'post_type'      => 'post',
2885        'orderby'        => 'date',
2886        'order'          => 'DESC',
2887        'fields'         => 'ids',
2888    );
2889
2890    $latest_draft_id = get_posts( $args );
2891
2892    $cached_blog_id  = get_current_blog_id();
2893    $cached_draft_id = reset( $latest_draft_id );
2894
2895    return $cached_draft_id;
2896}
2897
2898/**
2899 * Will return true if the primary domain is not a WPCOM domain.
2900 *
2901 * @return bool
2902 */
2903function wpcom_launchpad_is_ssl_task_visible() {
2904    return ! wpcom_launchpad_is_primary_domain_wpcom();
2905}
2906
2907/**
2908 * Checks if the current site primary domain is a WPCOM domain.
2909 *
2910 * @return bool Will return true if the primary domain is a WPCOM domain.
2911 */
2912function wpcom_launchpad_is_primary_domain_wpcom() {
2913    if ( ! ( new Automattic\Jetpack\Status\Host() )->is_wpcom_platform() ) {
2914        return false;
2915    }
2916
2917    $url  = home_url();
2918    $host = wp_parse_url( $url, PHP_URL_HOST );
2919
2920    // If site_slug ends with .wpcomstaging.com return true
2921    return str_ends_with( $host, '.wpcomstaging.com' );
2922}
2923
2924/**
2925 * Returns the goals for the current site to be used as a prop for the
2926 * 'wpcom_launchpad_mark_task_complete' event.
2927 *
2928 * Ensures the current user actually owns the current site, to make sure we
2929 * aren't recording goals for random sites like public-api.wordpress.com
2930 * (the action which triggers this event could have been triggered by a
2931 * public-api call.
2932 *
2933 * @return string Command separated list of goals.
2934 */
2935function wpcom_get_current_site_goals_for_tracks() {
2936    if ( ! current_user_can( 'manage_options' ) ) {
2937        return '';
2938    }
2939
2940    $site_goals = get_option( 'site_goals', array() );
2941    return implode( ',', $site_goals );
2942}
2943
2944/**
2945 * Checks if the subscribe block has been added or enabled.
2946 *
2947 * @return bool True if the task is completed, false otherwise.
2948 */
2949function wpcom_launchpad_has_added_subscribe_block() {
2950
2951    if ( get_option( 'jetpack_subscriptions_subscribe_post_end_enabled', false ) || get_option( 'jetpack_subscriptions_subscribe_navigation_enabled', false ) ) {
2952        return true;
2953    }
2954
2955    return wpcom_launchpad_is_task_option_completed( array( 'id' => 'add_subscribe_block' ) );
2956}
2957
2958/**
2959 * Will return true if the user can set up social connections.
2960 *
2961 * @return bool
2962 */
2963function wpcom_launchpad_is_jetpack_social_available() {
2964    return ! ( new Automattic\Jetpack\Status() )->is_private_site();
2965}