get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- single-row existence probe on admin screens only. $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s LIMIT 1", ADDON_BOSS_PRODUCT_OPTIONS_META ) ); } /** * The four steps with their live state. Each carries its one next action as * plain words + a URL; an action that cannot be resolved yet (step 3 before * anything is attached) is simply absent until it can. * * @return array */ function addon_boss_onboarding_steps(): array { $sets_done = (bool) addon_boss_option_sets(); $pid = addon_boss_onboarding_attached_product_id(); $attach_done = $pid > 0; $seen_done = (bool) get_option( ADDON_BOSS_ONBOARDING_SEEN ); $order_done = (bool) get_option( ADDON_BOSS_ONBOARDING_ORDERED ); // The storefront link both later steps point at: the attached product's own // page, once it has one shoppers can reach. $view_url = ''; if ( $attach_done && 'publish' === get_post_status( $pid ) ) { $view_url = (string) get_permalink( $pid ); } return array( array( 'id' => 'sets', 'done' => $sets_done, 'title' => __( 'Build your first option set', 'addon-boss' ), 'sub' => __( 'Ready-made shapes are waiting, or build your own.', 'addon-boss' ), 'act' => __( 'Start your first set', 'addon-boss' ), 'url' => admin_url( 'admin.php?page=addon-boss-option-sets' ), ), array( 'id' => 'attach', 'done' => $attach_done, 'title' => __( 'Attach it to a product', 'addon-boss' ), 'sub' => __( 'Open a product and look for its Product options tab.', 'addon-boss' ), 'act' => __( 'Open your products', 'addon-boss' ), 'url' => admin_url( 'edit.php?post_type=product' ), ), array( 'id' => 'seen', 'done' => $seen_done, 'title' => __( 'See it on the product page', 'addon-boss' ), 'sub' => '' !== $view_url || $seen_done ? '' : __( 'Once a published product carries a set, its page link appears here.', 'addon-boss' ), 'act' => __( 'View the product page', 'addon-boss' ), 'url' => $view_url, ), array( 'id' => 'order', 'done' => $order_done, 'title' => __( 'Place a test order', 'addon-boss' ), 'sub' => __( 'Order it like a shopper would; any test payment will do.', 'addon-boss' ), 'act' => __( 'Order it yourself', 'addon-boss' ), 'url' => $view_url, ), ); } /** * The checklist card on the AddOnBoss landing screen. House card language: * the bb-editor frame, ticks that fill as the store really progresses, one * quiet dismiss that never comes back. */ function addon_boss_onboarding_card(): void { if ( ! addon_boss_onboarding_active() || ! current_user_can( 'manage_woocommerce' ) ) { return; } $steps = addon_boss_onboarding_steps(); $done = count( array_filter( array_column( $steps, 'done' ) ) ); $all_done = count( $steps ) === $done; $dismiss = wp_nonce_url( admin_url( 'admin-post.php?action=addon_boss_dismiss_onboarding' ), 'addon_boss_dismiss_onboarding' ); echo '
'; echo '
' . esc_html__( 'Your first product with options', 'addon-boss' ) . ''; echo '' . esc_html( sprintf( /* translators: 1: steps done, 2: steps in total */ __( '%1$d of %2$d done', 'addon-boss' ), $done, count( $steps ) ) ) . ''; echo '✕'; echo '
'; echo '
    '; $bb_lead = true; // The FIRST open step's action is the card's primary and wears the house coral. foreach ( $steps as $bb_step ) { echo '
  1. '; echo ''; echo '' . esc_html( $bb_step['title'] ) . ''; if ( ! $bb_step['done'] && '' !== $bb_step['sub'] ) { echo '' . esc_html( $bb_step['sub'] ) . ''; } echo ''; if ( ! $bb_step['done'] && '' !== $bb_step['url'] ) { echo '' . esc_html( $bb_step['act'] ) . ''; } if ( ! $bb_step['done'] ) { $bb_lead = false; } echo '
  2. '; } echo '
'; if ( $all_done ) { echo '

' . esc_html__( 'All four done: your options are live and selling. Dismiss this guide whenever you like.', 'addon-boss' ) . '

'; } echo '
'; } /** * The compact echo on the Option Sets library while it is empty: the same four * steps as one quiet trail line, so the empty state says where this first set * is taking the merchant. No actions here; the page itself is step one. */ function addon_boss_onboarding_trail(): void { if ( ! addon_boss_onboarding_active() ) { return; } echo '

'; $bb_i = 0; foreach ( addon_boss_onboarding_steps() as $bb_step ) { ++$bb_i; echo ''; echo ''; echo esc_html( $bb_step['title'] ); echo ''; } echo '

'; } add_action( 'addon_boss_option_sets_empty_state', 'addon_boss_onboarding_trail' ); /** Dismiss for good: one option write, swept by the uninstall cleanup. */ function addon_boss_handle_dismiss_onboarding(): void { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'addon-boss' ) ); } check_admin_referer( 'addon_boss_dismiss_onboarding' ); update_option( ADDON_BOSS_ONBOARDING_DISMISSED, 'dismissed', false ); $back = wp_get_referer(); wp_safe_redirect( $back ? $back : admin_url( 'admin.php?page=addon-boss' ) ); exit; } add_action( 'admin_post_addon_boss_dismiss_onboarding', 'addon_boss_handle_dismiss_onboarding' ); // ------------------------------------------------------------------ // The activation pointer: the two cheap, conventional bridges from // "Plugin activated" to the landing screen. Without them the merchant // is stranded on the Plugins screen hunting a menu named BossMage. // ------------------------------------------------------------------ /** * A "Get started" action link on the plugin's own row of the Plugins screen, * standing before core's Deactivate. The one place every merchant certainly * looks right after activating is that row, so that is where the door goes. * * @param array $links The row's action links. * @return array The same links with Get started in front. */ function addon_boss_plugin_action_links( $links ): array { // A named key, not array_unshift: core prints each link inside a span // classed by its key, and a numeric key would class it "0". return array( 'bb-get-started' => '' . esc_html__( 'Get started', 'addon-boss' ) . '', ) + (array) $links; } add_filter( 'plugin_action_links_' . plugin_basename( ADDON_BOSS_FILE ), 'addon_boss_plugin_action_links' ); /** * The one-time doorway notice after activation: what is ready, and where to * begin. It shows while the activation marker reads '1' and the checklist is * both alive and completely untouched; the first real tick settles the marker * to 'done' (a merchant mid-checklist needs no doorway), dismissal writes * 'dismissed', and neither state ever shows it again, reactivation included. */ function addon_boss_activation_pointer_notice(): void { if ( '1' !== get_option( ADDON_BOSS_ACTIVATION_NOTICE ) || ! current_user_can( 'manage_woocommerce' ) ) { return; } if ( ! addon_boss_onboarding_active() ) { return; // The checklist was dismissed: the merchant asked for no guides. } foreach ( addon_boss_onboarding_steps() as $bb_step ) { if ( $bb_step['done'] ) { // Progress exists, so the doorway has been found. Settle the marker // for good; later admin loads skip the state probe entirely. update_option( ADDON_BOSS_ACTIVATION_NOTICE, 'done', false ); return; } } $dismiss = wp_nonce_url( admin_url( 'admin-post.php?action=addon_boss_dismiss_activation_notice' ), 'addon_boss_dismiss_activation_notice' ); echo '

'; echo esc_html__( 'AddOnBoss is ready.', 'addon-boss' ); echo ' ' . esc_html__( 'Set up your first product options', 'addon-boss' ) . ''; echo '

'; // Core's dismiss X only hides the box for the one page load; this makes the // X mean it. One capturing listener, planted with the notice itself; the // nonced URL rides the notice's own markup. addon_boss_inline_js( 'document.addEventListener( "click", function ( e ) {' . 'if ( ! e.target || ! e.target.closest || ! e.target.closest( "[data-bb-aob-hello] .notice-dismiss" ) ) { return; }' . 'var n = e.target.closest( "[data-bb-aob-hello]" );' . 'if ( n && n.getAttribute( "data-dismiss-url" ) ) { fetch( n.getAttribute( "data-dismiss-url" ), { credentials: "same-origin" } ); }' . '} );' ); } add_action( 'admin_notices', 'addon_boss_activation_pointer_notice' ); /** Dismissed is forever: the marker flips and the notice never returns. */ function addon_boss_handle_dismiss_activation_notice(): void { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'addon-boss' ) ); } check_admin_referer( 'addon_boss_dismiss_activation_notice' ); update_option( ADDON_BOSS_ACTIVATION_NOTICE, 'dismissed', false ); $back = wp_get_referer(); wp_safe_redirect( $back ? $back : admin_url( 'plugins.php' ) ); exit; } add_action( 'admin_post_addon_boss_dismiss_activation_notice', 'addon_boss_handle_dismiss_activation_notice' ); /** * Step 3's flag: the first storefront render of a product that offers options. * Runs just after the engine's renderer on the same hook, mirrors its "is * there anything to show" test, and writes exactly once ever. */ function addon_boss_onboarding_note_render(): void { if ( get_option( ADDON_BOSS_ONBOARDING_SEEN ) ) { return; } global $product; if ( ! $product instanceof WC_Product || ! $product->is_purchasable() ) { return; } if ( ! addon_boss_standalone_option_sets( $product ) && ! addon_boss_standalone_product_fields( $product ) ) { return; } update_option( ADDON_BOSS_ONBOARDING_SEEN, $product->get_id(), false ); } add_action( 'woocommerce_before_add_to_cart_button', 'addon_boss_onboarding_note_render', 12 ); /** * Step 4's flag: an order line carrying options or fields exists. Rides the * same hook the engine writes its order meta on, one priority later. * * @param WC_Order_Item_Product $item The order line (unused; the values decide). * @param string $key Cart item key (unused). * @param array $values The cart line the order line is built from. */ function addon_boss_onboarding_note_order( $item, $key = '', $values = array() ): void { if ( get_option( ADDON_BOSS_ONBOARDING_ORDERED ) ) { return; } if ( empty( $values[ ADDON_BOSS_STANDALONE_OPTIONS_KEY ] ) && empty( $values[ ADDON_BOSS_PRODUCT_FIELDS_KEY ] ) ) { return; } update_option( ADDON_BOSS_ONBOARDING_ORDERED, '1', false ); } add_action( 'woocommerce_checkout_create_order_line_item', 'addon_boss_onboarding_note_order', 20, 3 ); /** * The next action a notice should carry at a given moment, or an empty array * when the checklist is dismissed or the moment's step is already done. * * set_saved a set was just saved or imported and nothing is attached yet: * the answer is "Now attach it to a product". * attached options just landed on a product and no page has been seen: * the answer is "View it on the product page". * * @param string $moment Which moment just happened. * @param int $product_id The product involved, where the moment has one. * @return array{l:string,u:string}|array */ function addon_boss_onboarding_next_action( string $moment, int $product_id = 0 ): array { if ( ! addon_boss_onboarding_active() ) { return array(); } if ( 'set_saved' === $moment && 0 === addon_boss_onboarding_attached_product_id() ) { return array( 'l' => __( 'Now attach it to a product', 'addon-boss' ), 'u' => admin_url( 'edit.php?post_type=product' ), ); } if ( 'attached' === $moment && ! get_option( ADDON_BOSS_ONBOARDING_SEEN ) && $product_id > 0 && 'publish' === get_post_status( $product_id ) ) { $url = (string) get_permalink( $product_id ); if ( '' !== $url ) { return array( 'l' => __( 'View it on the product page', 'addon-boss' ), 'u' => $url, ); } } return array(); } /** * After options land on a product from its own edit screen, say what comes * next. Runs after the engine's save on the same hook; the notice itself shows * on the reload the save redirects to, then never again for that save. * * @param WC_Product $product The product just saved. */ function addon_boss_onboarding_note_attach( $product ): void { if ( ! $product instanceof WC_Product || ! addon_boss_onboarding_active() || get_option( ADDON_BOSS_ONBOARDING_SEEN ) ) { return; } if ( ! addon_boss_product_option_config( $product ) ) { return; } set_transient( 'addon_boss_first_attach_notice_' . get_current_user_id(), $product->get_id(), MINUTE_IN_SECONDS ); } add_action( 'woocommerce_admin_process_product_object', 'addon_boss_onboarding_note_attach', 20 ); /** Print (once) the just-attached notice with its next action. */ function addon_boss_onboarding_attach_notice(): void { $pid = (int) get_transient( 'addon_boss_first_attach_notice_' . get_current_user_id() ); if ( $pid <= 0 ) { return; } delete_transient( 'addon_boss_first_attach_notice_' . get_current_user_id() ); $action = addon_boss_onboarding_next_action( 'attached', $pid ); if ( ! $action ) { return; } echo '

'; echo esc_html__( 'Your options are on this product.', 'addon-boss' ); echo ' ' . esc_html( $action['l'] ) . ''; echo '

'; } add_action( 'admin_notices', 'addon_boss_onboarding_attach_notice' );