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