Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.55% covered (danger)
18.55%
23 / 124
20.00% covered (danger)
20.00%
1 / 5
CRAP
n/a
0 / 0
wpcom_should_show_ai_assistant_banner
29.41% covered (danger)
29.41%
5 / 17
0.00% covered (danger)
0.00%
0 / 1
53.56
wpcom_ai_assistant_banner_tracks_props
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
12
wpcom_render_ai_assistant_banner
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_maybe_add_ai_assistant_banner
41.67% covered (danger)
41.67%
5 / 12
0.00% covered (danger)
0.00%
0 / 1
7.18
wpcom_dismiss_ai_assistant_banner
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * AI Assistant Banner for the WordPress.com dashboard.
4 *
5 * Displays a banner prompting Business/Commerce admins to enable the AI assistant.
6 * Works on both Simple and Atomic sites via cross-platform utilities.
7 *
8 * @package automattic/jetpack-mu-wpcom
9 */
10
11use Automattic\Jetpack\Jetpack_Mu_Wpcom\Expiry_Notices\Expiry_Data;
12use Automattic\Jetpack\Status\Host;
13
14// The audience helpers live in the AI Launchpad feature, loaded by a different
15// loader method; require them directly so the props can never silently degrade.
16require_once __DIR__ . '/../ai-launchpad/helpers.php';
17
18/**
19 * Determines whether the AI assistant banner should be shown to the current user.
20 *
21 * @return bool
22 */
23function wpcom_should_show_ai_assistant_banner() {
24    if ( ! current_user_can( 'manage_options' ) ) {
25        return false;
26    }
27
28    // Check dismissal early — most users will have dismissed over time.
29    if ( get_user_meta( get_current_user_id(), 'wpcom_ai_assistant_banner_dismissed', true ) ) {
30        return false;
31    }
32
33    if ( ! function_exists( 'wpcom_site_has_feature' ) || ! wpcom_site_has_feature( WPCOM_Features::BIG_SKY ) ) {
34        return false;
35    }
36
37    // Don't show on Big Sky sites.
38    if ( wpcom_has_blog_sticker( 'big-sky-enabled', get_wpcom_blog_id() ) ) {
39        return false;
40    }
41
42    // Don't show if AI assistant is already enabled.
43    if ( class_exists( 'Big_Sky' ) && get_option( 'big_sky_enable', '1' ) ) {
44        return false;
45    }
46
47    if ( function_exists( 'get_user_attribute' ) ) {
48        $user_id = get_current_user_id();
49
50        // Don't show if user has opted out of AI features.
51        if ( '1' === get_user_attribute( $user_id, 'ai_features_opted_out' ) ) {
52            return false;
53        }
54
55        // Don't show if user has unsubscribed from AI Tips emails.
56        if ( '1' === get_user_attribute( $user_id, 'notifications_wpcom_email_ai_tips' ) ) {
57            return false;
58        }
59    }
60
61    return true;
62}
63
64/**
65 * Tracks props shared by the banner's three events, per the AI events standard.
66 *
67 * @param string $screen_id The current admin screen id.
68 * @return array<string,string>
69 */
70function wpcom_ai_assistant_banner_tracks_props( $screen_id ) {
71    $purchases = function_exists( 'wpcom_expiry_get_purchases' ) ? wpcom_expiry_get_purchases() : array();
72    // Real plan bundles only: the expiry picker's slug fallback would also match
73    // add-ons such as Professional Email Premium ("premium" substring).
74    $bundles  = wp_list_filter( (array) $purchases, array( 'product_type' => 'bundle' ) );
75    $plan     = class_exists( Expiry_Data::class ) ? Expiry_Data::pick_primary_plan_purchase( $bundles ) : null;
76    $has_slug = $plan && isset( $plan->product_slug ) && is_string( $plan->product_slug ) && '' !== $plan->product_slug;
77
78    return array(
79        'channel'      => 'web',
80        'surface'      => 'dashboard',
81        // Deliberately the WP screen id, not $pagenow — AI Launchpad's hardcoded
82        // 'admin.php' uses the other vocabulary for the same standard prop.
83        'screen'       => (string) $screen_id,
84        // Two buckets, mirroring AI Launchpad: this package ships only on Simple and
85        // Atomic (wpcomsh, mu-wpcom-plugin). Add a self-hosted branch if the banner
86        // ever moves into the Jetpack plugin.
87        'site_type'    => ( new Host() )->is_wpcom_simple() ? 'simple' : 'atomic',
88        // The AI events standard wants the string "none" over an empty value.
89        'product_slug' => $has_slug ? $plan->product_slug : 'none',
90        'is_test'      => function_exists( 'wpcom_ai_launchpad_is_test' ) && wpcom_ai_launchpad_is_test() ? 'true' : 'false',
91        'is_a11n'      => function_exists( 'wpcom_ai_launchpad_is_a11n' ) && wpcom_ai_launchpad_is_a11n() ? 'true' : 'false',
92    );
93}
94
95/**
96 * Renders the AI assistant banner markup.
97 */
98function wpcom_render_ai_assistant_banner() {
99    $site_slug = wpcom_get_site_slug();
100    $cta_url   = wpcom_get_calypso_origin() . '/sites/' . $site_slug . '/settings/ai-tools';
101    $nonce     = wp_create_nonce( 'dismiss_ai_assistant_banner' );
102    ?>
103    <div id="wpcom-ai-assistant-banner" class="notice is-dismissible" style="border-left-color: #3858E9;" data-nonce="<?php echo esc_attr( $nonce ); ?>">
104        <div class="wpcom-ai-assistant-banner__layout">
105            <svg width="24" height="24" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
106                <path d="M391.528 188.061L309.455 159.75C276.997 148.597 251.403 123.003 240.25 90.5451L211.939 8.47185C208.079 -2.82395 191.921 -2.82395 188.061 8.47185L159.75 90.5451C148.597 123.003 123.003 148.597 90.5451 159.75L8.47185 188.061C-2.82395 191.921 -2.82395 208.079 8.47185 211.939L90.5451 240.25C123.003 251.403 148.597 276.997 159.75 309.455L188.061 391.528C191.921 402.824 208.079 402.824 211.939 391.528L240.25 309.455C251.403 276.997 276.997 251.403 309.455 240.25L391.528 211.939C402.824 208.079 402.824 191.921 391.528 188.061ZM295.728 206.077L254.692 220.232C238.391 225.809 225.666 238.677 220.089 254.835L205.934 295.871C203.932 301.591 195.925 301.591 193.923 295.871L179.768 254.835C174.191 238.534 161.323 225.809 145.165 220.232L104.129 206.077C98.4093 204.075 98.4093 196.068 104.129 194.066L145.165 179.911C161.466 174.334 174.191 161.466 179.768 145.308L193.923 104.272C195.925 98.5523 203.932 98.5523 205.934 104.272L220.089 145.308C225.666 161.609 238.534 174.334 254.692 179.911L295.728 194.066C301.448 196.068 301.448 204.075 295.728 206.077Z" fill="#3858E9"/>
107            </svg>
108            <div class="wpcom-ai-assistant-banner__body">
109                <div class="wpcom-ai-assistant-banner__text">
110                    <strong><?php esc_html_e( 'Bring AI to your site.', 'jetpack-mu-wpcom' ); ?></strong><br>
111                    <?php esc_html_e( 'Opt-in to get site-aware assistance where you write and design.', 'jetpack-mu-wpcom' ); ?>
112                </div>
113                <a href="<?php echo esc_url( $cta_url ); ?>" class="button-secondary">
114                    <?php esc_html_e( 'Enable AI assistant', 'jetpack-mu-wpcom' ); ?>
115                </a>
116            </div>
117        </div>
118    </div>
119    <style>
120        #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__layout {
121            display: flex;
122            align-items: center;
123            gap: 16px;
124            padding: 4px 0;
125        }
126        #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__layout > svg {
127            flex-shrink: 0;
128        }
129        #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__body {
130            display: flex;
131            align-items: center;
132            gap: 16px;
133            flex: 1;
134        }
135        #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__text {
136            flex: 1;
137        }
138        #wpcom-ai-assistant-banner .button-secondary {
139            white-space: nowrap;
140        }
141        #wpcom-ai-assistant-banner .notice-dismiss {
142            top: 50%;
143            transform: translateY(-50%);
144        }
145        #wpcom-ai-assistant-banner .notice-dismiss:before {
146            content: "\2715";
147            font-family: inherit;
148            color: #787c82;
149            background: none;
150        }
151        @media ( max-width: 782px ) {
152            #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__layout {
153                align-items: flex-start;
154                gap: 8px;
155            }
156            #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__layout > svg {
157                width: 18px;
158                height: 18px;
159                margin-top: 4px;
160            }
161            /* Stack the copy and button, keeping both aligned to the text column rather than under the icon. */
162            #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__body {
163                flex-direction: column;
164                align-items: flex-start;
165                gap: 12px;
166            }
167            #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__text {
168                font-size: 13px;
169                line-height: 1.5;
170            }
171            #wpcom-ai-assistant-banner .wpcom-ai-assistant-banner__text strong {
172                font-size: 14px;
173            }
174            /* Keep the dismiss control top-right like every other notice, lined up with the title. */
175            #wpcom-ai-assistant-banner .notice-dismiss {
176                top: 8px;
177                transform: none;
178            }
179        }
180    </style>
181    <?php
182}
183
184/**
185 * Conditionally adds the AI assistant banner on the dashboard screen.
186 */
187function wpcom_maybe_add_ai_assistant_banner() {
188    $screen = get_current_screen();
189    if ( ! $screen || 'dashboard' !== $screen->id ) {
190        return;
191    }
192
193    if ( ! wpcom_should_show_ai_assistant_banner() ) {
194        return;
195    }
196
197    add_action( 'admin_notices', 'wpcom_render_ai_assistant_banner', 1 );
198
199    $asset_handle = jetpack_mu_wpcom_enqueue_assets( 'ai-assistant-banner', array( 'js' ) );
200    wp_localize_script(
201        $asset_handle,
202        'wpcomAiAssistantBanner',
203        array( 'trackProps' => wpcom_ai_assistant_banner_tracks_props( $screen->id ) )
204    );
205}
206add_action( 'current_screen', 'wpcom_maybe_add_ai_assistant_banner' );
207
208/**
209 * AJAX handler to dismiss the AI assistant banner.
210 */
211function wpcom_dismiss_ai_assistant_banner() {
212    check_ajax_referer( 'dismiss_ai_assistant_banner', 'nonce' );
213    update_user_meta( get_current_user_id(), 'wpcom_ai_assistant_banner_dismissed', '1' );
214    wp_send_json_success( null, 200, JSON_UNESCAPED_SLASHES );
215}
216add_action( 'wp_ajax_dismiss_ai_assistant_banner', 'wpcom_dismiss_ai_assistant_banner' );