Using the Admin Notes Inbox in WooCommerce

As of WooCommerce 4.6, the new home screen that was introduced in WooCommerce 4.3 has become the default experience for merchants. With the aim of empowering merchants to manage their stores smartly and with confidence, the new home screen serves up a store’s most important to-dos in one, central place. One powerful tool for this is the Admin Notes Inbox.

In this post, we’ll take a look at Admin Notes in WooCommerce and share a few best practices for using the Inbox as a developer. Finally, we’ll walk through the process of creating a simple extension that displays an Admin Note when it is activated.

What are Admin Notes?

Admin Notes are meant for displaying insightful information about your WooCommerce store, extensions, activity, and achievements. They’re also useful for displaying information that can help with the day-to-day tasks of managing and optimizing a store.

Best practices for using Admin Notes

A good general rule is to use Admin Notes for information that is:

  1. Timely
  2. Relevant
  3. Useful

With that in mind, you might consider using Admin Notes to celebrate a particular milestone that a merchant has passed, or to provide additional guidance about using a specific feature or flow. Conversely, you shouldn’t use Admin Notes to send repeated messages about the same topic or target all users with a note that is only relevant to a subset of merchants. It’s okay to use Admin Notes for specific promotions, but you shouldn’t abuse the system. Use your best judgement and remember the home screen is meant to highlight a store’s most important actionable tasks.

Tutorial: Building an Extension that Uses Admin Notes

You can follow along with this walkthrough step-by-step, but there is also an annotated example repository with a completed example available here.

Note: The steps in this walkthrough rely on Docker and Node. If you don’t have these both installed, you can install them by visiting the links below.

We’ll also be working with the command line a bit, so it will be helpful to have a Unix-flavored shell installed as well, such as Bash, Zsh, etc.

Project Set Up

Use the WooCommerce Admin Extension Generator to Scaffold an Extension

Note: For our example, I’ll use the extension generator that comes with WooCommerce Admin. It’s a great tool for getting up and running with a new extension because it is geared toward modern WooCommerce development and handles so much of the setup for you. If you have an existing workflow for extension creation, you can use that too. We won’t be using any JavaScript in this example—just working with the standard PHP API.

In a clean directory, clone the woocommerce/woocommerce-admin repository.

> git clone https://github.com/woocommerce/woocommerce-admin.git

From the command line, cd into the cloned repository and install the project dependencies with npm install and composer install:

> cd woocommerce-admin
> npm install
> composer install

The woocommerce-admin repository has a node script that will scaffold out a modern WooCommerce extension for you. Let’s use that to create the skeleton of our extension.

> npm run create-wc-extension

After answering the prompts, you’ll have a scaffolded extension in a sibling folder beside your WC-Admin folder.

Use wp-env to Manage Your Development Environment

Note: If you have an existing workflow for managing your WordPress and WooCommerce environments, you can bring the scaffolded extension into that workflow and skip ahead to Writing the Extension. The remaining setup steps will configure a containerized instance of an empty WooCommerce store that you can use for extension development.

From the command line, cd into your scaffolded extension directory and run:

> npm install -g @wordpress/env

This installs the wp-env virtualization tool as a global executable on your local machine. This is a utility that will let us spin up and work with a containerized WordPress development environment. If you have never worked with wp-env before, you can learn more about it in this blog post and in the documentation.

Once wp-env is installed, create a new file in your directory and call it .wp-env.json. This is a configuration file that will tell wp-env how to set up our WordPress environment.

{
    "core": null,
    "plugins": [
        "https://downloads.wordpress.org/plugin/woocommerce.4.5.1.zip",
        "."
    ]
}

Now that we have told wp-env how to set up our WooCommerce environment, run wp-env start to start your servers. This may take a while the first time you run it.

Once your WordPress servers are up and running, you can run npm start from the command line. This will build your extension and continuously watch the files for changes as you work.

> npm run env start
✓ WordPress started. (in 15s 429ms)
> npm start

webpack is watching the files…

Live Reload listening on port 35729

Hash: af48a1c717c5fc0825be
Version: webpack 4.43.0
Time: 1452ms
Built at: 05/20/2020 2:37:59 PM
          Asset       Size  Chunks                   Chunk Names
index.asset.php  161 bytes   index  [emitted]        index
       index.js   84.6 KiB   index  [emitted]        index
   index.js.map   82.6 KiB   index  [emitted] [dev]  index
Entrypoint index = index.js index.js.map index.asset.php
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {index} [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {index} [built]
[./src/index.js] 10.8 KiB {index} [built]
[./src/top-rated-products.js] 1.87 KiB {index} [built]
[@babel/runtime/regenerator] external {"this":"regeneratorRuntime"} 42 bytes {index} [built]
[@woocommerce/components] external {"this":["window","wc","components"]} 42 bytes {index} [built]
[@wordpress/element] external {"this":["wp","element"]} 42 bytes {index} [built]
[@wordpress/hooks] external {"this":["wp","hooks"]} 42 bytes {index} [built]
[lodash] external {"this":"lodash"} 42 bytes {index} [built]
    + 23 hidden modules

If you browse to localhost:8888, log in and visit your WooCommerce dashboard, you’ll see the “Hello World” message from the scaffolded extension in your browser’s console.

Now we’re ready to build our extension!


Writing the Extension

Go ahead and open up your extension’s main PHP file. It should be [your-plugin-name].php, or whichever name you supplied the extension generator.

There is already a bit of content in this file. The function and JS registration were created by the extension generator, and they’ll streamline the process of adding JavaScript-based functionality to your extension.

/**
 * Register the JS.
 */

function add_extension_register_script() {
  if ( ! class_exists( 'AutomatticWooCommerceAdminLoader' ) || ! AutomatticWooCommerceAdminLoader::is_admin_page() ) {
    return;
  }
  ...
}

For now, we’ll leave this section alone and simply append our example to this file.

Update Your Extension Headers

The first thing we’ll want to do (after getting our project under version control) is update the headers in our extension’s primary PHP file. These headers contain metadata that WordPress and WooCommerce use for various purposes.

Open up the your-extension-title.php file and fill in any additional headers:

/**
 * Plugin Name: admin-note-example
 * Plugin URI: https://woocommerce.com/
 * Description: An example extension demonstrating Admin Notes in WooCommerce.
 * Author: Automattic
 * Author URI: https://woocommerce.com/
 * Text Domain: admin-note-example
 * Version: 1.0.0
 *
 * @package WC_Admin
 * Tested up to: 5.4
 * WC tested up to: 4.2
 * WC requires at least: 2.6
 *
 * Copyright: © 2020 WooCommerce
 * License: GNU General Public License v3.0
 * License URI: http://www.gnu.org/licenses/gpl-3.0.html
 *
 * Woo: [your-upgrade-code]
 */

Note: While it’s not critical to have all of these fields for our particular example, it’s good practice to include this information in extensions destined for a production environment.

Check ABSPATH

Before writing any more PHP for our extension, let’s take a moment to put a quick security check in place. Add the following conditional to your file:

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

This is a simple test to prevent unauthorized direct access to the extension’s file.

Import Dependencies

Before we can use Admin Notes in our extension, we need to import their supporting classes.

use AutomatticWooCommerceAdminNotesWC_Admin_Note;
use AutomatticWooCommerceAdminNotesWC_Admin_Notes;

Create Class and Function Stubs

For simplicity, we’ll encapsulate the majority of our example’s logic in a single class. In a real-world extension, you’d likely encapsulate this logic into its own class and import it alongside your extension’s other supporting classes. Lets’ start by stubbing out a few functions and creating a constant to hold our note name.

class My_Great_Extension {

  const NOTE_NAME = 'mge-activation-notice';

  public static function add_activity_panel_inbox_welcome_note() {

  }

  public static function remove_activity_panel_inbox_welcome_notes() {

  }
}

Register Activation and Deactivation Hooks

Since our extension is going to be making updates when it activates, let’s hook into the activation process and define a function to be called when the merchant activates our extension. After your class definition, add the following:

function my_great_extension_activate() {
    // We'll fill this in next.
}
register_activation_hook( __FILE__, 'my_great_extension_activate' );

function my_great_extension_deactivate() {
  // We'll fill this in next.
}
register_deactivation_hook( __FILE__, 'my_great_extension_deactivate');

Define the Activation Logic

Next, we’ll define what we want to happen when the extension activates. Let’s fill out the implementation of the add_activity_panel_inbox_welcome_note() function in our My_Great_Extension class:

public static function add_activity_panel_inbox_welcome_note() {
  if ( ! class_exists( 'AutomatticWooCommerceAdminNotesWC_Admin_Notes') ) {
    return;
  }

  if ( ! class_exists( 'WC_Data_Store' ) ) {
    return;
  }

  $data_store = WC_Data_Store::load('admin-note');
}

Here we’re putting a few checks in place to make sure that Admin Notes and the WC Data Store are available. Then we’re loading up the Admin Note records from the Data Store and assigning it to a variable.

Before our extension creates any new notes, it’s a good practice to first check for any existing notes of the same type. This helps us make sure we’re not creating duplicate notes, which can cause clutter and confusion for merchants.

$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
  foreach( (array) $note_ids as $note_id ) {
    $note           = WC_Admin_Notes::get_note( $note_id );
    $content_data   = $note->get_content_data();
    if ( property_exists( $content_data, 'getting_started' ) ) {
      return;
    }
  }

Here we’re querying the Admin Note records in the Data Store for any with a name that matches our note name. If there are any matches with a property in their content_data called getting_started, we’ll bail out. This means that our note type already exists. We’ll go over content_data in more detail just a bit later.

Note: As of WooCommerce Admin v1.6.0, the NoteTraits class provides much of the repetitive logic related to adding and deleting Admin Notes.

Next, let’s write a bit of logic to generate some of the content that our Admin Note will display. Our example shows the time that our extension was activated, but that’s just one simple example of countless use cases.

$activated_time = current_time( 'timestamp', 0);
$activated_time_formatted = date( 'F jS', $activated_time );

Let’s instantiate a new Admin Note object so we can start tailoring its properties and content:

$note = new WC_Admin_Note();

With our new Admin Note object instantiated, we’ll start by setting its title property:

$note->set_title( 'Getting Started' );

Next, we’ll use some of the variables we created earlier to populate our note’s content:

$note->set_content(
  sprintf(
    'Extension activated on %s.', $activated_time_formatted
  )
);

In addition to content, Admin Notes also support structured content. You can use the content_data property for all sorts of things. It is backed by a longtext column in the database.

$note->set_content_data( (object) array(
   'getting_started'       => true,
   'activated'             => $activated_time,
   'activated_formatted'   => $activated_time_formatted
) );

Next, let’s set the note’s type property. Note types are defined as enum-style class constants in the WC_Admin_Note class. Available note types are error, warning, update, info, and marketing. When selecting a note type, be aware that the error and update result in the note being shown as a Store Alert, not in the Inbox. It’s best to avoid using these types of notes unless you absolutely need to.

We’ll use an info type for our note:

$note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_INFORMATIONAL );

Admin Notes also support a few different layouts. You can specify banner, plain, or thumbnail as the layout. If you’re interested in seeing the different layouts in action, @fermarichal coded a simple plugin that you can install to experiment with them.

We’ll choose plain as our layout, but it’s also the default, so we could leave this property alone and the effect would be the same:

$note->set_layout( 'plain' );

If we have an image that we want to add to our Admin Note, we can specify it here as well. This property ultimately renders as the src attribute on an img tag, so use a string here.

 $note->set_image( '' );

Next, we’ll set the values for our Admin Note’s name and source properties. As a best practice, you should store your extension’s name (slug) in the source property of the note. You can use the name property to support multiple sub-types of notes. This gives you a handy way of namespacing your notes and managing them at both a high and low level.

$note->set_source( 'admin-note-example');
$note->set_name( self::NOTE_NAME );

Admin Notes can support 0, 1, or 2 actions (buttons). You can use these actions to capture events that trigger asynchronous processes or help the merchant navigate to a particular view to complete a step, or even simply to provide an external link for further information.

$note->add_action(
  'settings', 'Open Settings', '?page=wc-settings&tab=general'
);
$note->add_action(
  'learn_more', 'Learn More', 'https://example.com'
);

Once we’re satisfied with the properties on our Admin Note, we need to save it to make sure those values are written to the database:

$note->save();

Putting it all together, our add_activity_panel_inbox_welcome_note() function should look like this:

public static function add_activity_panel_inbox_welcome_note() {

        // Check for Admin Note support
        if ( ! class_exists( 'AutomatticWooCommerceAdminNotesWC_Admin_Notes') ) {
            return;
        }

        // Make sure the WooCommerce Data Store is available
        if ( ! class_exists( 'WC_Data_Store' ) ) {
            return;
        }

        // Load the Admin Notes from the WooCommerce Data Store
        $data_store = WC_Data_Store::load('admin-note');

        // Check for existing notes that match our note name and content data.
        //  This ensures we don't create a duplicate note.
        $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );
        foreach( (array) $note_ids as $note_id ) {
            $note           = WC_Admin_Notes::get_note( $note_id );
            $content_data   = $note->get_content_data();
            if ( property_exists( $content_data, 'getting_started' ) ) {
                return;
            }
        }

        // Our welcome note will include information about when the extension
        //   was activated.  This is just for demonstration.  You might include
        //   other logic here depending on what data your note should contain.
        $activated_time = current_time( 'timestamp', 0);
        $activated_time_formatted = date( 'F jS', $activated_time );

        // Instantiate a new Admin_Note object
        $note = new WC_Admin_Note();

        // Set our note's title.
        $note->set_title( 'Getting Started' );

        // Set our note's content.
        $note->set_content(
            sprintf(
                'Extension activated on %s.', $activated_time_formatted
            )
        );

        // In addition to content, notes also support structured content.
        //  You can use this property to re-localize notes on the fly, but
        //  that is just one use.  You can store other data here too.  This
        //  is backed by a longtext column in the database.
        $note->set_content_data( (object) array(
            'getting_started'       => true,
            'activated'             => $activated_time,
            'activated_formatted'   => $activated_time_formatted
        ) );

        // Set the type of the note.  Note types are defined as enum-style
        // constants in the WC_Admin_Note class.  Available note types are:
        //   error, warning, update, info, marketing
        $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_INFORMATIONAL );

        // Set the type of layout the note uses.  Supported layout types are:
        //   'banner', 'plain', 'thumbnail'
        $note->set_layout( 'plain' );

        // Set the image for the note.  This property renders as the src
        //   attribute for an img tag, so use a string here.
        $note->set_image( '' );


        // Set the note name and source.  You should store your extension's
        //   name (slug) in the source property of the note.  You can use
        //   the name property of the note to support multiple sub-types of
        //   notes.  This also gives you a handy way of namespacing your notes.
        $note->set_source( 'inbox-note-example');
        $note->set_name( self::NOTE_NAME );

        // Add action buttons to the note.  A note can support 0, 1, or 2 actions.
        //   The first parameter is the action name, which can be used for event handling.
        //   The second parameter renders as the label for the button.
        //   The third parameter is an optional URL for actions that require navigation.
        $note->add_action(
            'settings', 'Open Settings', '?page=wc-settings&tab=general'
        );
        $note->add_action(
            'learn_more', 'Learn More', 'https://example.com'
        );

        // Save the note to lock in our changes.
        $note->save();
    }

Define Deactivation Logic

As a best practice, extensions that use Admin Notes should clean up behind themselves when the merchant deactivates them by deleting any notes they created. Let’s fill in the details of our remove_activity_panel_inbox_welcome_notes() function to do that:

public static function remove_activity_panel_inbox_welcome_notes() {
  if ( ! class_exists( 'AutomatticWooCommerceAdminNotesWC_Admin_Notes' ) ) {
    return;
  }
  WC_Admin_Notes::delete_notes_with_name( self::NOTE_NAME );
}

Note: For simplicity, we’re deleting all notes that match our note name, but you can customize your removal function to ensure you are deleting the right notes.

That’s it!

Your extension should now be generating Admin Notes. Assuming your server is still running, if you toggle your extension’s activation on the WordPress Admin Plugins screen, you should be able to browse to your WooCommerce Dashboard and see the notes your extension created. You can try turning the extension on and off a few times to see the Admin Notes appear and disappear.

Next Steps

What we covered in this example is just a sampling of what you can do with Admin Notes in your own extensions. Take a look at the Sales Record Note and the Settings Note in WooCommerce to see other examples of this feature in use.

Since the new home screen is becoming the default experience for merchants, Admin Notes are the best way to get the most important information in front of merchants just when they need it. If you’re not already using them, start today.

Feel free to reach out to us in Slack if you have questions or run into trouble!


Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.