Commit ca3f5cf67bf for woocommerce
commit ca3f5cf67bf5c04bd6c057a0e7f0dcf09b259747
Author: Thilina Pituwala <thilina.hasantha@gmail.com>
Date: Fri Oct 9 13:29:38 2026 +1100
Update/woomkt 991 add utm params to woocommercecom subscription inbox note (#69465)
* Add UTM params to WooCommerce.com subscription inbox note links
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add changelog entry for subscription inbox note UTM params
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use utm_source=pu for subscription inbox note links
Match the Plugins screen subscription notices, which already use
utm_source=pu, so both placements report under the same source.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Make has_action_url() static
The helper only reads the note passed to it, so it is pure and the
backend convention requires it to be static.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Test that up-to-date expired notes are not rewritten
Add a test that fails if has_action_url() stops matching the stored
Renew Subscription URL, and explain why it compares with esc_url_raw().
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/update-woomkt-991-inbox-subscription-note-utm b/plugins/woocommerce/changelog/update-woomkt-991-inbox-subscription-note-utm
new file mode 100644
index 00000000000..dacd33ec05e
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-woomkt-991-inbox-subscription-note-utm
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Add UTM params to WooCommerce.com subscription Inbox note links; not visible to merchants.
+
diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/WooSubscriptionsNotes.php b/plugins/woocommerce/src/Internal/Admin/Notes/WooSubscriptionsNotes.php
index 12c56657caf..6160c26c84f 100644
--- a/plugins/woocommerce/src/Internal/Admin/Notes/WooSubscriptionsNotes.php
+++ b/plugins/woocommerce/src/Internal/Admin/Notes/WooSubscriptionsNotes.php
@@ -23,6 +23,7 @@ class WooSubscriptionsNotes {
const SUBSCRIPTION_NOTE_NAME = 'wc-admin-wc-helper-subscription';
const NOTIFY_WHEN_DAYS_LEFT = 60;
const BUMP_THRESHOLDS = array( 60, 45, 20, 7, 1 ); // days.
+ private const UTM_SOURCE = 'pu';
/**
* Hook all the things.
@@ -320,7 +321,13 @@ class WooSubscriptionsNotes {
$note->add_action(
'enable-autorenew',
__( 'Enable Autorenew', 'woocommerce' ),
- 'https://woocommerce.com/my-account/my-subscriptions/?utm_medium=product'
+ add_query_arg(
+ array(
+ 'utm_source' => self::UTM_SOURCE,
+ 'utm_campaign' => 'pu_inbox_enable_autorenew',
+ ),
+ 'https://woocommerce.com/my-account/my-subscriptions/'
+ )
);
$note->set_content( $note_content );
$note->set_content_data( $note_content_data );
@@ -338,11 +345,18 @@ class WooSubscriptionsNotes {
$product_page = $subscription['product_url'];
$expires = intval( $subscription['expires'] );
$expires_date = gmdate( 'F jS', $expires );
+ $renew_url = '' === (string) $product_page ? '' : add_query_arg(
+ array(
+ 'utm_source' => self::UTM_SOURCE,
+ 'utm_campaign' => 'pu_inbox_renew',
+ ),
+ $product_page
+ );
$note = $this->find_note_for_product_id( $product_id );
if ( $note ) {
$note_content_data = $note->get_content_data();
- if ( $note_content_data->expired ) {
+ if ( $note_content_data->expired && self::has_action_url( $note, 'renew-subscription', $renew_url ) ) {
// We've already got a full fledged expired note for this. Bail.
// Expired notes' content don't change with time.
return;
@@ -383,11 +397,32 @@ class WooSubscriptionsNotes {
$note->add_action(
'renew-subscription',
__( 'Renew Subscription', 'woocommerce' ),
- $product_page
+ $renew_url
);
$note->save();
}
+ /**
+ * Whether a note already has the given action with the given URL.
+ *
+ * Lets expired notes saved before a link change pick up the new URL on the next refresh.
+ *
+ * @param Note $note The note to check.
+ * @param string $action_name The action name.
+ * @param string $url The expected action URL.
+ * @return bool
+ */
+ private static function has_action_url( Note $note, string $action_name, string $url ): bool {
+ foreach ( (array) $note->get_actions() as $action ) {
+ if ( isset( $action->name, $action->query ) && $action_name === $action->name ) {
+ // Note::add_action() stores the URL through esc_url_raw(), so compare against the same form.
+ return esc_url_raw( $url ) === $action->query;
+ }
+ }
+
+ return false;
+ }
+
/**
* For each active subscription on this site, checks the expiration date and creates/updates/deletes notes.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Notes/WooSubscriptionsNotesTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Notes/WooSubscriptionsNotesTest.php
new file mode 100644
index 00000000000..30920a020d8
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Notes/WooSubscriptionsNotesTest.php
@@ -0,0 +1,141 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Admin\Notes;
+
+use Automattic\WooCommerce\Admin\Notes\Note;
+use Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the WooSubscriptionsNotes class.
+ */
+class WooSubscriptionsNotesTest extends WC_Unit_Test_Case {
+
+ /**
+ * The System Under Test.
+ *
+ * @var WooSubscriptionsNotes
+ */
+ private $sut;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->sut = new WooSubscriptionsNotes();
+ }
+
+ /**
+ * @testdox Should link the expiring note's Enable Autorenew action to My Subscriptions with inbox UTM params.
+ */
+ public function test_expiring_note_action_has_inbox_utm_params(): void {
+ $this->sut->add_or_update_subscription_expiring(
+ array(
+ 'product_id' => 101,
+ 'product_name' => 'Test Extension',
+ 'expires' => time() + 30 * DAY_IN_SECONDS,
+ )
+ );
+
+ $action = $this->get_note_action( 101, 'enable-autorenew' );
+
+ $this->assertSame(
+ 'https://woocommerce.com/my-account/my-subscriptions/?utm_source=pu&utm_campaign=pu_inbox_enable_autorenew',
+ $action->query,
+ 'Enable Autorenew should link to My Subscriptions with the inbox UTM params'
+ );
+ }
+
+ /**
+ * @testdox Should link the expired note's Renew Subscription action to the product page with inbox UTM params.
+ */
+ public function test_expired_note_action_has_inbox_utm_params(): void {
+ $this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 102 ) );
+
+ $action = $this->get_note_action( 102, 'renew-subscription' );
+
+ $this->assertSame(
+ 'https://woocommerce.com/products/test-extension/?utm_source=pu&utm_campaign=pu_inbox_renew',
+ $action->query,
+ 'Renew Subscription should link to the product page with the inbox UTM params'
+ );
+ }
+
+ /**
+ * @testdox Should update the action URL of an expired note saved before the UTM params were added.
+ */
+ public function test_existing_expired_note_gets_new_action_url(): void {
+ $this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 103 ) );
+ $note = $this->sut->find_note_for_product_id( 103 );
+ $note->clear_actions();
+ $note->add_action( 'renew-subscription', 'Renew Subscription', 'https://woocommerce.com/products/test-extension/' );
+ $note->save();
+
+ $this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 103 ) );
+
+ $action = $this->get_note_action( 103, 'renew-subscription' );
+ $this->assertSame(
+ 'https://woocommerce.com/products/test-extension/?utm_source=pu&utm_campaign=pu_inbox_renew',
+ $action->query,
+ 'A stale expired note should pick up the new action URL'
+ );
+ }
+
+ /**
+ * @testdox Should not rewrite an expired note whose Renew Subscription URL is already current.
+ */
+ public function test_up_to_date_expired_note_is_not_rewritten(): void {
+ $this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 105 ) );
+ $action_id = $this->get_note_action( 105, 'renew-subscription' )->id;
+
+ $this->sut->add_or_update_subscription_expired( $this->get_expired_subscription( 105 ) );
+
+ $this->assertSame( $action_id, $this->get_note_action( 105, 'renew-subscription' )->id, 'An expired note with the current URL should not be rewritten' );
+ }
+
+ /**
+ * @testdox Should leave the Renew Subscription URL empty when the subscription has no product page.
+ */
+ public function test_expired_note_without_product_page_has_empty_action_url(): void {
+ $subscription = $this->get_expired_subscription( 104 );
+ $subscription['product_url'] = '';
+
+ $this->sut->add_or_update_subscription_expired( $subscription );
+
+ $this->assertSame( '', $this->get_note_action( 104, 'renew-subscription' )->query, 'UTM params should not be added to an empty URL' );
+ }
+
+ /**
+ * Builds an expired subscription record as returned by the WooCommerce.com API.
+ *
+ * @param int $product_id The product ID.
+ * @return array
+ */
+ private function get_expired_subscription( int $product_id ): array {
+ return array(
+ 'product_id' => $product_id,
+ 'product_name' => 'Test Extension',
+ 'product_url' => 'https://woocommerce.com/products/test-extension/',
+ 'expires' => time() - 10 * DAY_IN_SECONDS,
+ );
+ }
+
+ /**
+ * Loads the saved subscription note for a product and returns one of its actions.
+ *
+ * @param int $product_id The product ID.
+ * @param string $action_name The action name.
+ * @return object
+ */
+ private function get_note_action( int $product_id, string $action_name ): object {
+ $note = $this->sut->find_note_for_product_id( $product_id );
+ $this->assertInstanceOf( Note::class, $note, 'A subscription note should exist for the product' );
+
+ $actions = wp_list_filter( $note->get_actions(), array( 'name' => $action_name ) );
+ $this->assertCount( 1, $actions, "The note should have one {$action_name} action" );
+
+ return reset( $actions );
+ }
+}