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