Docs

Hooks and Filters Reference

12 min readUpdated October 31, 2026

WC Account Portal has a set of WordPress filters and one action that let you change what the portal shows without editing the plugin. This page lists every hook, with its arguments, when it runs and a short example. The other Developers docs go deeper into the most used ones.

Where to put the code#

Add the snippets on this page to your child theme’s functions.php or to a code snippets plugin. Do not edit the plugin’s files: your changes would be lost on the next update.

  • Add the hooks at the top level of the file, not inside a check such as is_account_page(). Some of them (wap_dashboard_widgets, wap_audiences, wap_wholesale_pages, wap_supported_plugins) are also read on the settings page in the admin, so they must be registered there too.
  • The plugin needs PHP 8.3, so arrow functions, typed closures and static fn all work.
  • Every hook name starts with wap_. The plugin uses the text domain wc-account-portal; use your own text domain for your own strings.
Tip

Return the first argument unchanged when your code has nothing to do. Every filter on this page expects its first argument back, even when you only want to read it.

All hooks at a glance#

HookTypeAreaWhat it changes
wap_dashboard_widgetsFilterDashboardRegisters your own Dashboard widgets
wap_dashboard_widget_{id}ActionDashboardPrints one of your widgets
wap_dashboard_tilesFilterDashboardThe Summary boxes
wap_store_has_downloadsFilterDashboardWhether the store sells downloadable products
wap_next_paymentFilterDashboardThe Next payment widget’s data
wap_checklist_stepsFilterDashboardThe Getting started checklist
wap_buy_again_productsFilterDashboardThe Buy again widget’s products
wap_profile_badgesFilterDashboardBadges on the profile card
wap_order_timeline_stepsFilterOrdersOrder progress steps
wap_order_tracking_itemsFilterOrdersShipment tracking numbers
wap_order_tracking_listFilterOrdersWhether the portal prints its tracking list
wap_audiencesFilterMenu and layoutsThe audiences you can pick in the settings
wap_user_audiencesFilterMenu and layoutsWhich audiences a customer belongs to
wap_menu_parent_endpointsFilterMenuWhich menu item a single page highlights
wap_wholesale_pagesFilterMenuWholesale plugin pages offered in Menu Items
wap_logo_imageFilterBrandingThe logo image
wap_logout_urlFilterPortal pageThe Sign out link
wap_is_third_party_takeoverFilterPortal pageKeeps the portal off a page
wap_keep_assetFilterPortal pageKeeps a theme file when Theme Styles drops them
wap_login_rate_limitsFilterLogin and securityFailed sign-in limits in the Account Nav Widget
wap_reset_rate_limitsFilterLogin and securityPassword reset limits in the Account Nav Widget
wap_client_ipFilterLogin and securityThe visitor IP used by those limits
wap_captcha_fail_openFilterLogin and securityWhat happens when the CAPTCHA provider cannot be reached
wap_bundle_shield_translationsFilterLogin and securityBundled translations for Nahnu Login Shield
wap_supported_pluginsFilterAdminThe Supported plugins list on the Integrations tab

Dashboard#

wap_dashboard_widgets#

apply_filters( 'wap_dashboard_widgets', array $widgets )
  • $widgets (array<string,string>): widget ID => title. Empty by default.
  • Returns the same array with your widgets added.

Runs whenever the plugin builds the Dashboard layout: on the customer’s Dashboard and on WooCommerce > Account Portal > Dashboard. IDs go through sanitize_key(), and an ID that matches a built-in widget (welcome, summary, next_payment, checklist, shortcuts, recent_orders, buy_again, help, custom) is ignored. New widgets are off until you switch them on in the Dashboard tab.

wap_dashboard_widget_{id}#

do_action( 'wap_dashboard_widget_' . $id, int $user_id, array $nav_items )
  • $user_id (int): the signed-in customer.
  • $nav_items (array): the menu items this customer sees.

Prints the widget registered with wap_dashboard_widgets. Output is buffered: when your callback prints nothing, no empty grid cell is added.

add_filter( 'wap_dashboard_widgets', static function ( array $widgets ): array {
	$widgets['store_news'] = __( 'Store news', 'my-textdomain' );
	return $widgets;
} );

add_action( 'wap_dashboard_widget_store_news', static function ( int $user_id, array $nav_items ): void {
	printf(
		'<div class="wap-dash-card"><div class="wap-dash-card-head"><h2>%s</h2></div><div class="wap-custom-widget-body"><p>%s</p></div></div>',
		esc_html__( 'Store news', 'my-textdomain' ),
		esc_html__( 'Free shipping on all orders this week.', 'my-textdomain' )
	);
}, 10, 2 );

See Adding Your Own Dashboard Widgets and Tiles for the full guide.

wap_dashboard_tiles#

apply_filters( 'wap_dashboard_tiles', array $tiles, int $user_id )
  • $tiles (array[]): the Summary boxes, in display order. Each is an array with endpoint, icon, count, label, url and value.
  • $user_id (int): the customer.
  • Returns the boxes to show, in the order to show them.

Runs when the Summary boxes widget is on. A box needs a label and either an endpoint or a url. Without a url key it links to the account endpoint; value (a string) is shown instead of the number. Details and examples are in Adding Your Own Dashboard Widgets and Tiles.

wap_store_has_downloads#

apply_filters( 'wap_store_has_downloads', bool $has_downloads )
  • $has_downloads (bool): whether any published product or variation is downloadable. The answer is cached for 12 hours and cleared when a product is saved.

Used by the Downloads box when it is set to Automatic: hidden when the store sells nothing downloadable. A customer who still has downloads from an older order always sees the box.

// The store sells downloads through another plugin: always treat it as a downloads store.
add_filter( 'wap_store_has_downloads', '__return_true' );

wap_next_payment#

apply_filters( 'wap_next_payment', array|null $data, int $user_id )
  • $data (array|null): null when nothing was found, otherwise an array with time (Unix timestamp of the renewal), amount (HTML, for example from wc_price()), items (product names), url (the Manage link) and more (how many other active subscriptions).
  • $user_id (int): the customer.

Runs when the Next payment widget is on. The widget shows only when the result is an array with a non-empty time. The plugin fills it from WooCommerce Subscriptions first; its own callback at priority 5 fills it from YITH WooCommerce Subscription, SUMO Subscriptions and Subscriptions for WooCommerce (WP Swings) when nothing was found yet. Use it to supply the data from another subscription plugin:

add_filter( 'wap_next_payment', static function ( $data, int $user_id ) {
	if ( is_array( $data ) && ! empty( $data['time'] ) ) {
		return $data; // already found by the plugin.
	}
	$next = (int) get_user_meta( $user_id, 'my_next_renewal', true ); // your plugin's data.
	if ( $next <= 0 ) {
		return $data;
	}
	return array(
		'time'   => $next,
		'amount' => wc_price( (float) get_user_meta( $user_id, 'my_renewal_amount', true ) ),
		'items'  => __( 'Coffee club', 'my-textdomain' ),
		'url'    => wc_get_account_endpoint_url( 'my-subscriptions' ),
		'more'   => 0,
	);
}, 10, 2 );

The Other active subscriptions link under the card only shows when more is above 0 and the WooCommerce Subscriptions page (subscriptions) is in the menu. A Custom URL item that points to /my-account/subscriptions/ counts as that page.

wap_checklist_steps#

apply_filters( 'wap_checklist_steps', array $steps, int $user_id )
  • $steps (array[]): each step is array( 'label' => string, 'done' => bool, 'url' => string ).
  • $user_id (int): the customer.

Runs when the Getting started widget is on. Steps without a label are dropped. A step that is not done links to its url. When every step is done (or there are none), the checklist is not shown.

add_filter( 'wap_checklist_steps', static function ( array $steps, int $user_id ): array {
	$steps[] = array(
		'label' => __( 'Join our newsletter', 'my-textdomain' ),
		'done'  => (bool) get_user_meta( $user_id, 'my_newsletter_optin', true ),
		'url'   => home_url( '/newsletter/' ),
	);
	return $steps;
}, 10, 2 );

wap_buy_again_products#

apply_filters( 'wap_buy_again_products', WC_Product[] $products, int $user_id )
  • $products (WC_Product[]): products from the customer’s completed orders that can be bought now, newest first.
  • $user_id (int): the customer.

Runs when the Buy again widget is on. Anything that is not a WC_Product is skipped; an empty array hides the widget. See Changing Buy Again Products.

wap_profile_badges#

apply_filters( 'wap_profile_badges', string[] $names, int $user_id )
  • $names (string[]): the customer’s active membership names, for example “Gold Club, until May 3, 2027”.
  • $user_id (int): the customer.

Runs when the Welcome widget uses the Profile card. Tags are stripped, duplicates and empty strings are removed and at most three badges show. See Adding Profile Badges.

Orders#

wap_order_timeline_steps#

apply_filters( 'wap_order_timeline_steps', array $steps, WC_Order $order )
  • $steps (array[]): each step is array( 'label' => string, 'state' => string, 'date' => string ). state is done, current, todo or problem; date is already formatted text (or '').
  • $order (WC_Order): the order.

Runs on a single order page in the portal when Order progress is on (WooCommerce > Account Portal > General > Orders & empty pages). Steps without a label are dropped and an unknown state becomes todo. The steps only show when there are at least two. See Customising Order Progress Steps and Tracking.

wap_order_tracking_items#

apply_filters( 'wap_order_tracking_items', array $items, WC_Order $order )
  • $items (array[]): each item is array( 'provider' => string, 'number' => string, 'url' => string, 'date' => int ). date is the Unix timestamp the parcel shipped (0 when unknown).
  • $order (WC_Order): the order.

The plugin reads tracking from WooCommerce Shipment Tracking and Advanced Shipment Tracking (free and Pro). Use this filter for other shipping plugins. Items without a number are dropped. When an order has tracking, the progress shows a Shipped step.

wap_order_tracking_list#

apply_filters( 'wap_order_tracking_list', bool $show )
  • $show (bool): true unless Advanced Shipment Tracking is active and Shipment tracking on the order page (Advanced Shipment Tracking) is set to the plugin’s own block.

When the portal prints its tracking list under the steps, it removes the tracking plugin’s own block from the order page so the numbers show once.

// Always use the portal's tracking list.
add_filter( 'wap_order_tracking_list', '__return_true' );

wap_audiences#

apply_filters( 'wap_audiences', array $audiences )
  • $audiences (array<string,string>): key => name. By default every WordPress role; supported wholesale plugins add their customer groups.

The list in Menu Items > Who sees it and Dashboard > Layout for. Built once per request.

wap_user_audiences#

apply_filters( 'wap_user_audiences', string[] $keys, WP_User $user )
  • $keys (string[]): the audiences the user is in. By default their roles.
  • $user (WP_User): the signed-in user. Only called for logged-in users.

Use the two filters together to add groups that are not roles. See Creating Custom Audiences for Menu Items.

wap_menu_parent_endpoints#

apply_filters( 'wap_menu_parent_endpoints', array $map )
  • $map (array<string,string>): endpoint of a single page => menu item ID to highlight. By default view-order => orders, view-subscription and subscription-payment-method => subscriptions, and the single pages of SUMO Subscriptions, Subscriptions for WooCommerce and the supported wholesale plugins.

When a customer opens a single item (an order, a subscription), the menu highlights its list.

add_filter( 'wap_menu_parent_endpoints', static function ( array $map ): array {
	$map['view-ticket'] = 'tickets'; // a support plugin's single ticket page highlights "Tickets".
	return $map;
} );

wap_wholesale_pages#

apply_filters( 'wap_wholesale_pages', array $pages )
  • $pages (array[]): each page is array( 'id' => string, 'label' => string, 'icon' => string, 'plugin' => string, 'desc' => string, 'wholesale' => bool ). id is the account endpoint, icon a built-in icon name, and wholesale decides whether Who sees it is set to wholesale customers when the page is added.

The wholesale and B2B pages offered under Menu Items > Extra pages: add in one click. Only id is required: a page without one is skipped, and missing keys are filled in (label defaults to the id, icon to storefront, plugin and desc to empty, wholesale to false).

Portal page and branding#

wap_logo_image#

apply_filters( 'wap_logo_image', array|null $logo, string $mode )
  • $logo (array|null): null to use the logos set in Appearance. Return array( 'src' => string, 'width' => int, 'height' => int ) to use another image (width and height can be 0 when unknown, for example an SVG).
  • $mode (string): light, dark or dark_only. The portal asks for light (the main logo) and dark_only (the dark-mode logo; an empty src means “no separate dark logo”).

The same values are used by the logo preview in the settings.

add_filter( 'wap_logo_image', static function ( $logo, string $mode ) {
	if ( 'light' === $mode ) {
		return array(
			'src'    => get_stylesheet_directory_uri() . '/images/account-logo.svg',
			'width'  => 0,
			'height' => 0,
		);
	}
	return $logo; // keep the dark-mode logo from the settings.
}, 10, 2 );

wap_logout_url#

apply_filters( 'wap_logout_url', string $url )
  • $url (string): the portal’s sign-out address, /logout/ (or /{lang}/logout/ with WPML or Polylang in a non-default language).

Used by the Sign out button in the portal and in the Account Nav Widget.

// Use WooCommerce's own sign-out link (it carries a nonce).
add_filter( 'wap_logout_url', static fn( string $url ): string => wc_logout_url() );

wap_is_third_party_takeover#

apply_filters( 'wap_is_third_party_takeover', bool $takeover )
  • $takeover (bool): false by default. Return true to keep the portal off the current page.

When it returns true, the account page keeps your theme’s template, the portal’s CSS and JavaScript are not loaded, and the Account Nav Widget’s drawer is not added. The plugin already does this for the cart and checkout, Elementor Canvas and similar blank templates, and funnel steps. Conditional tags such as is_page() work here.

add_filter( 'wap_is_third_party_takeover', static function ( bool $takeover ): bool {
	// Replace "my_builder_preview" with the query var your page builder uses for its editor.
	return $takeover || isset( $_GET['my_builder_preview'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
} );

wap_keep_asset#

apply_filters( 'wap_keep_asset', bool $keep, string $handle, string $src )
  • $keep (bool): false by default.
  • $handle (string): the script or style handle.
  • $src (string): its URL.

Only runs when Don’t load the theme’s CSS and JavaScript on the account pages (faster pages) is on (General > Theme Styles). The portal then drops every file from the themes folder and the block editor’s front-end styles (wp-block-library, global-styles and similar). Return true to keep one.

add_filter( 'wap_keep_asset', static function ( bool $keep, string $handle, string $src ): bool {
	return $keep || 'my-theme-fonts' === $handle;
}, 10, 3 );

Login and security#

wap_login_rate_limits#

apply_filters( 'wap_login_rate_limits', array $limits )
  • $limits (array): per_user (10), per_ip (20) and window (900 seconds). 0 turns a limit off; the window is at least 60 seconds.

Failed sign-ins allowed in the Account Nav Widget’s drawer before it refuses more attempts. The WooCommerce login form on the account page is not counted here.

add_filter( 'wap_login_rate_limits', static fn( array $limits ): array => array_merge( $limits, array( 'per_user' => 5, 'window' => 30 * MINUTE_IN_SECONDS ) ) );

wap_reset_rate_limits#

apply_filters( 'wap_reset_rate_limits', array $limits )
  • $limits (array): per_email (3), per_ip (10) and window (900 seconds).

Password reset requests from the Account Nav Widget’s drawer. Every request counts, not only failed ones.

wap_client_ip#

apply_filters( 'wap_client_ip', string $ip )
  • $ip (string): $_SERVER['REMOTE_ADDR'].

The IP the two rate limits above count per visitor. Behind a proxy or CDN every visitor can share the same address, so return the real one:

add_filter( 'wap_client_ip', static function ( string $ip ): string {
	// Only when the site is really behind Cloudflare: anyone can send this header otherwise.
	$real = filter_var( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ?? '' ), FILTER_VALIDATE_IP );
	return $real ? (string) $real : $ip;
} );

wap_captcha_fail_open#

apply_filters( 'wap_captcha_fail_open', bool $allow, string $provider )
  • $allow (bool): true by default.
  • $provider (string): hcaptcha, recaptcha_v2, recaptcha_v3 or turnstile.

Runs only when the CAPTCHA provider cannot be reached (a timeout, a server error or an unreadable reply). By default the sign-in, registration or reset goes through, so an outage does not lock every customer out; a token the provider rejects is always refused. Return false to refuse instead:

add_filter( 'wap_captcha_fail_open', '__return_false' );

wap_bundle_shield_translations#

apply_filters( 'wap_bundle_shield_translations', bool $enabled )
  • $enabled (bool): true by default.

When Nahnu Login Shield is installed, the portal offers its own translations of Login Shield’s visitor-facing text for the portal’s languages. Official translation files in wp-content/languages/plugins/ always win. Turn the bundle off with:

add_filter( 'wap_bundle_shield_translations', '__return_false' );

Admin#

wap_supported_plugins#

apply_filters( 'wap_supported_plugins', array $plugins )
  • $plugins (array[]): each is array( 'name' => string, 'url' => string, 'active' => bool, 'support' => string, 'endpoint' => string, 'tested' => string ). support is tab (My Account tab), page (portal page, add in one click), wholesale, order (tracking in the order progress) or anything else for “Own account page”.

Only changes the Supported plugins list on the Integrations tab. It does not add support for a plugin.

Share:

Was this helpful?