Skip to content

Commit 4aea4d4

Browse files
authored
Merge branch 'trunk' into issue/791-only-set-paid-date-on-renewal-when-needed
2 parents 60a1d9d + ce1eec5 commit 4aea4d4

9 files changed

+262
-109
lines changed

assets/js/admin/admin-pointers.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
jQuery( function ( $ ) {
2+
let observer = null;
3+
24
if ( arePointersEnabled() ) {
3-
setTimeout( showSubscriptionPointers, 800 ); // give TinyMCE a chance to finish loading
5+
observer = new MutationObserver( showSubscriptionPointers );
6+
7+
observer.observe( document.getElementById( 'poststuff' ), {
8+
attributes: true,
9+
childList: true,
10+
characterData: false,
11+
subtree:true,
12+
} );
413
}
514

615
$( 'select#product-type' ).on( 'change', function () {
@@ -62,5 +71,9 @@ jQuery( function ( $ ) {
6271
pointer: 'wcs_pointer',
6372
action: 'dismiss-wp-pointer',
6473
} );
74+
75+
if ( observer ) {
76+
observer.disconnect();
77+
}
6578
}
6679
} );

changelog.txt

+5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66
* Update - Reduced duplicate queries when fetching multiple subscription related orders types.
77
* Update - Removed unnecessary get_time() calls to reduce redundant get_last_order() queries in the Subscriptions list table.
88
* Update - Improved performance on the Orders list table when rendering the Subscription Relationship column.
9+
* Fix - Added support for previewing payment retry emails in WooCommerce email settings.
10+
* Fix - Updated subscription email item table template to align with WooCommerce 9.7 email improvements.
911
* Fix - Prevent PHP warning on cart page shipping method updates by removing unused method: maybe_restore_shipping_methods.
1012
* Fix - Removed unnecessary setting of renewal order paid date on status transition, relying on WooCommerce core behavior instead.
13+
* Fix - Ensure the order_awaiting_payment session arg is restored when loading a renewal cart from the session to prevent duplicate orders.
14+
* Fix - Ensure custom placeholders (time_until_renewal, customers_first_name) are included in customer notification email previews.
15+
* Fix - Correctly load product names with HTML on the cart and checkout shipping rates.
1116
* Dev - Fix Node version mismatch between package.json and .nvmrc (both are now set to v16.17.1).
1217

1318
= 8.0.1 - 2025-02-13 =

includes/class-wc-subscriptions-email-notifications.php

-57
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ public static function init() {
5353
// Add settings UI.
5454
add_filter( 'woocommerce_subscription_settings', [ __CLASS__, 'add_settings' ], 20 );
5555

56-
// Add admin notice.
57-
add_action( 'admin_notices', [ __CLASS__, 'maybe_add_admin_notice' ] );
58-
5956
// Bump settings update time whenever related options change.
6057
add_action( 'update_option_' . WC_Subscriptions_Admin::$option_prefix . self::$offset_setting_string, [ __CLASS__, 'set_notification_settings_update_time' ], 10, 3 );
6158
add_action( 'update_option_' . WC_Subscriptions_Admin::$option_prefix . self::$switch_setting_string, [ __CLASS__, 'set_notification_settings_update_time' ], 10, 3 );
@@ -307,58 +304,4 @@ public static function add_settings( $settings ) {
307304
WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_miscellaneous', $notification_settings, 'multiple_settings', 'sectionend' );
308305
return $settings;
309306
}
310-
311-
/**
312-
* Maybe add an admin notice to inform the store manager about the existance of the notifications feature.
313-
*/
314-
public static function maybe_add_admin_notice() {
315-
316-
// If the notifications feature is enabled, don't show the notice.
317-
if ( self::notifications_globally_enabled() ) {
318-
return;
319-
}
320-
321-
// Prevent showing the notice on the Subscriptions settings page.
322-
if ( isset( $_GET['page'], $_GET['tab'] ) && 'wc-settings' === $_GET['page'] && 'subscriptions' === $_GET['tab'] ) {
323-
return;
324-
}
325-
326-
$option_name = 'wcs_hide_customer_notifications_notice';
327-
$nonce = '_wcsnonce';
328-
$action = 'wcs_hide_customer_notifications_notice_action';
329-
330-
// First, check if the notice is being dismissed.
331-
$nonce_argument = sanitize_text_field( wp_unslash( $_GET[ $nonce ] ?? '' ) );
332-
if ( isset( $_GET[ $action ], $nonce_argument ) && wp_verify_nonce( $nonce_argument, $action ) ) {
333-
update_option( $option_name, 'yes' );
334-
wp_safe_redirect( remove_query_arg( [ $action, $nonce ] ) );
335-
return;
336-
}
337-
338-
if ( 'yes' === get_option( $option_name ) ) {
339-
return;
340-
}
341-
342-
$admin_notice = new WCS_Admin_Notice( 'notice', array(), wp_nonce_url( add_query_arg( $action, 'dismiss' ), $action, $nonce ) );
343-
$notice_title = __( 'WooCommerce Subscriptions: Introducing customer email notifications!', 'woocommerce-subscriptions' );
344-
$notice_content = __( 'You can now send email notifications for subscription renewals, expirations, and free trials. Go to the "Customer Notifications" settings section to configure when your customers receive these important updates.', 'woocommerce-subscriptions' );
345-
$html_content = sprintf( '<p class="main"><strong>%1$s</strong></p><p>%2$s</p>', $notice_title, $notice_content );
346-
$admin_notice->set_html_content( $html_content );
347-
$admin_notice->set_actions(
348-
array(
349-
array(
350-
'name' => __( 'Manage settings', 'woocommerce-subscriptions' ),
351-
'url' => admin_url( 'admin.php?page=wc-settings&tab=subscriptions' ),
352-
'class' => 'button button-primary',
353-
),
354-
array(
355-
'name' => __( 'Learn more', 'woocommerce-subscriptions' ),
356-
'url' => 'https://woocommerce.com/document/subscriptions/subscriptions-notifications/',
357-
'class' => 'button',
358-
),
359-
)
360-
);
361-
362-
$admin_notice->display();
363-
}
364307
}

includes/class-wc-subscriptions-email.php

+18-19
Original file line numberDiff line numberDiff line change
@@ -241,25 +241,23 @@ public static function email_order_items_table( $order, $args = array() ) {
241241
$order = wc_get_order( $order );
242242
}
243243

244-
if ( is_a( $order, 'WC_Abstract_Order' ) ) {
245-
$show_download_links_callback = ( isset( $args['show_download_links'] ) && $args['show_download_links'] ) ? '__return_true' : '__return_false';
246-
$show_purchase_note_callback = ( isset( $args['show_purchase_note'] ) && $args['show_purchase_note'] ) ? '__return_true' : '__return_false';
244+
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
245+
return $items_table;
246+
}
247247

248-
unset( $args['show_download_links'] );
249-
unset( $args['show_purchase_note'] );
248+
$show_download_links_callback = ( isset( $args['show_download_links'] ) && $args['show_download_links'] ) ? '__return_true' : '__return_false';
249+
$show_purchase_note_callback = ( isset( $args['show_purchase_note'] ) && $args['show_purchase_note'] ) ? '__return_true' : '__return_false';
250250

251-
add_filter( 'woocommerce_order_is_download_permitted', $show_download_links_callback );
252-
add_filter( 'woocommerce_order_is_paid', $show_purchase_note_callback );
251+
unset( $args['show_download_links'] );
252+
unset( $args['show_purchase_note'] );
253253

254-
if ( function_exists( 'wc_get_email_order_items' ) ) { // WC 3.0+
255-
$items_table = wc_get_email_order_items( $order, $args );
256-
} else {
257-
$items_table = $order->email_order_items_table( $args );
258-
}
254+
add_filter( 'woocommerce_order_is_download_permitted', $show_download_links_callback );
255+
add_filter( 'woocommerce_order_is_paid', $show_purchase_note_callback );
259256

260-
remove_filter( 'woocommerce_order_is_download_permitted', $show_download_links_callback );
261-
remove_filter( 'woocommerce_order_is_paid', $show_purchase_note_callback );
262-
}
257+
$items_table = wc_get_email_order_items( $order, $args );
258+
259+
remove_filter( 'woocommerce_order_is_download_permitted', $show_download_links_callback );
260+
remove_filter( 'woocommerce_order_is_paid', $show_purchase_note_callback );
263261

264262
return $items_table;
265263
}
@@ -274,13 +272,14 @@ public static function email_order_items_table( $order, $args = array() ) {
274272
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.1
275273
*/
276274
public static function order_details( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) {
277-
278-
$order_items_table_args = array(
275+
$email_improvements_enabled = wcs_is_wc_feature_enabled( 'email_improvements' );
276+
$image_size = $email_improvements_enabled ? 48 : 32; // These image sizes are defaults for WC core emails. @see wc_get_email_order_items().
277+
$order_items_table_args = array(
279278
'show_download_links' => ( $sent_to_admin ) ? false : $order->is_download_permitted(),
280279
'show_sku' => $sent_to_admin,
281280
'show_purchase_note' => ( $sent_to_admin ) ? false : $order->has_status( apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) ) ),
282-
'show_image' => '',
283-
'image_size' => '',
281+
'show_image' => $email_improvements_enabled,
282+
'image_size' => array( $image_size, $image_size ),
284283
'plain_text' => $plain_text,
285284
);
286285

includes/class-wcs-cart-renewal.php

+45-2
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public function setup_hooks() {
113113
// Make sure renewal meta data persists between sessions
114114
add_filter( 'woocommerce_get_cart_item_from_session', array( &$this, 'get_cart_item_from_session' ), 10, 3 );
115115
add_action( 'woocommerce_cart_loaded_from_session', array( &$this, 'cart_items_loaded_from_session' ), 10 );
116+
add_action( 'woocommerce_cart_loaded_from_session', array( $this, 'restore_order_awaiting_payment' ), 10 );
116117

117118
// Make sure fees are added to the cart
118119
add_action( 'woocommerce_cart_calculate_fees', array( &$this, 'maybe_add_fees' ), 10, 1 );
@@ -248,14 +249,22 @@ public function maybe_setup_cart() {
248249
* @internal Core checkout uses order_awaiting_payment, Blocks checkout uses store_api_draft_order. Both validate the
249250
* cart hash to ensure the order matches the cart.
250251
*
251-
* @param int $order_id The order ID that is awaiting payment, or 0 to unset it.
252+
* @param int|WC_Order $order_id The order that is awaiting payment, or 0 to unset it.
252253
*/
253254
protected function set_order_awaiting_payment( $order_id ) {
255+
$order = null;
256+
257+
if ( is_a( $order_id, 'WC_Abstract_Order' ) ) {
258+
$order = $order_id;
259+
$order_id = $order->get_id();
260+
}
261+
254262
WC()->session->set( 'order_awaiting_payment', $order_id );
255263
WC()->session->set( 'store_api_draft_order', $order_id );
256264

257265
if ( $order_id ) {
258-
$this->set_cart_hash( $order_id );
266+
// To avoid needing to load the order object, pass it if available, otherwise pass the order ID.
267+
$this->set_cart_hash( $order ?? $order_id );
259268
}
260269
}
261270

@@ -1654,6 +1663,40 @@ public function set_renewal_order_cart_hash_on_block_checkout( $has_status, $ord
16541663
return $has_status;
16551664
}
16561665

1666+
/**
1667+
* Restores the order awaiting payment session args if the cart contains a subscription-related order.
1668+
*
1669+
* It's possible the that order_awaiting_payment and store_api_draft_order session args are not set if those session args are lost due
1670+
* to session destruction.
1671+
*
1672+
* This function checks the cart that is being loaded from the session and if the cart contains a subscription-related order and if the
1673+
* current user has permission to pay for it. If so, it restores the order awaiting payment session args.
1674+
*
1675+
* @param WC_Cart $cart The cart object.
1676+
*/
1677+
public function restore_order_awaiting_payment( $cart ) {
1678+
if ( ! is_a( $cart, WC_Cart::class ) ) {
1679+
return;
1680+
}
1681+
1682+
foreach ( $cart->get_cart() as $cart_item ) {
1683+
$order = $this->get_order( $cart_item );
1684+
1685+
if ( ! $order ) {
1686+
continue;
1687+
}
1688+
1689+
// If the current user has permission to pay for the order, restore the order awaiting payment session arg.
1690+
if ( $this->validate_current_user( $order ) ) {
1691+
$this->set_order_awaiting_payment( $order );
1692+
}
1693+
1694+
// Once we found an order, exit even if the user doesn't have permission to pay for it.
1695+
return;
1696+
1697+
}
1698+
}
1699+
16571700
/* Deprecated */
16581701

16591702
/**

includes/emails/class-wc-subscriptions-email-preview.php

+98-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,14 @@ public function prepare_email_for_preview( $email ) {
4949
case 'WCS_Email_Customer_Notification_Auto_Renewal':
5050
$email->set_object( $this->get_dummy_subscription() );
5151
break;
52+
case 'WCS_Email_Customer_Payment_Retry':
53+
case 'WCS_Email_Payment_Retry':
54+
$email->retry = $this->get_dummy_retry( $email->object );
55+
break;
5256
}
5357

58+
$this->add_placeholders( $email );
59+
5460
add_filter( 'woocommerce_mail_content', [ $this, 'clean_up_filters' ] );
5561

5662
return $email;
@@ -139,13 +145,55 @@ private function get_dummy_address() {
139145
return apply_filters( 'woocommerce_subscriptions_email_preview_dummy_address', $address, $this->email_type );
140146
}
141147

148+
/**
149+
* Creates a dummy retry for use when previewing failed subscription payment retry emails.
150+
*
151+
* @param WC_Order $order The order object to create a dummy retry for.
152+
* @return WCS_Retry The dummy retry object.
153+
*/
154+
private function get_dummy_retry( $order ) {
155+
156+
if ( ! class_exists( 'WCS_Retry_Manager' ) ) {
157+
return null;
158+
}
159+
160+
$order_id = is_a( $order, 'WC_Order' ) ? $order->get_id() : 12345;
161+
$retry_rule = WCS_Retry_Manager::rules()->get_rule( 1, $order_id );
162+
163+
if ( is_a( $retry_rule, 'WCS_Retry_Rule' ) ) {
164+
$interval = $retry_rule->get_retry_interval();
165+
$raw_retry_rule = $retry_rule->get_raw_data();
166+
} else {
167+
// If the retry rule is not found, use a default interval of 12 hours and an empty raw rule.
168+
$interval = 12 * HOUR_IN_SECONDS;
169+
$raw_retry_rule = [];
170+
}
171+
172+
return new WCS_Retry(
173+
[
174+
'status' => 'pending',
175+
'order_id' => $order_id,
176+
'date_gmt' => gmdate( 'Y-m-d H:i:s', time() + $interval ),
177+
'rule_raw' => $raw_retry_rule,
178+
]
179+
);
180+
}
181+
142182
/**
143183
* Check if the email being previewed is a subscription email.
144184
*
145-
* @return bool
185+
* Subscription emails include:
186+
* - WC_Subscriptions_Email::$email_classes - core subscription emails.
187+
* - WC_Subscriptions_Email_Notifications::$email_classes - subscription notification emails (pre-renewal emails).
188+
* - WCS_Email_Customer_Payment_Retry - customer payment retry emails.
189+
* - WCS_Email_Payment_Retry - admin payment retry emails.
190+
*
191+
* @return bool Whether the email being previewed is a subscription email.
146192
*/
147193
private function is_subscription_email() {
148-
return isset( WC_Subscriptions_Email::$email_classes[ $this->email_type ] ) || isset( WC_Subscriptions_Email_Notifications::$email_classes[ $this->email_type ] );
194+
return isset( WC_Subscriptions_Email::$email_classes[ $this->email_type ] )
195+
|| isset( WC_Subscriptions_Email_Notifications::$email_classes[ $this->email_type ] )
196+
|| in_array( $this->email_type, [ 'WCS_Email_Customer_Payment_Retry', 'WCS_Email_Payment_Retry' ], true );
149197
}
150198

151199
/**
@@ -204,4 +252,52 @@ public function allow_early_renewals_during_preview( $can_renew_early, $subscrip
204252

205253
return $can_renew_early;
206254
}
255+
256+
/**
257+
* Adds custom placeholders for subscription emails.
258+
*
259+
* @param WC_Email $email The email object.
260+
*/
261+
private function add_placeholders( $email ) {
262+
if ( ! isset( $email->placeholders ) ) {
263+
return;
264+
}
265+
266+
$placeholders = [];
267+
268+
switch ( $this->email_type ) {
269+
case 'WCS_Email_Customer_Notification_Subscription_Expiration':
270+
case 'WCS_Email_Customer_Notification_Manual_Trial_Expiration':
271+
case 'WCS_Email_Customer_Notification_Auto_Trial_Expiration':
272+
case 'WCS_Email_Customer_Notification_Manual_Renewal':
273+
case 'WCS_Email_Customer_Notification_Auto_Renewal':
274+
// Pull the real values from the email object (Order or Subscription) if available.
275+
if ( is_a( $email->object, 'WC_Subscription' ) ) {
276+
$time_until_renewal = $email->get_time_until_date( $email->object, 'next_payment' );
277+
$customer_first_name = $email->object->get_billing_first_name();
278+
} else {
279+
$time_until_renewal = human_time_diff( time(), time() + WEEK_IN_SECONDS );
280+
$customer_first_name = 'John';
281+
}
282+
283+
$placeholders['{time_until_renewal}'] = $time_until_renewal;
284+
$placeholders['{customers_first_name}'] = $customer_first_name;
285+
break;
286+
case 'WCS_Email_Customer_Payment_Retry':
287+
case 'WCS_Email_Payment_Retry':
288+
$retry_time = is_a( $email->retry, 'WCS_Retry' )
289+
? $email->retry->get_time()
290+
: time() + ( 12 * HOUR_IN_SECONDS );
291+
292+
$placeholders['{retry_time}'] = wcs_get_human_time_diff( $retry_time );
293+
break;
294+
}
295+
296+
// Merge placeholders without overriding existing ones, and only adding those in the email.
297+
$email->placeholders = wp_parse_args(
298+
$placeholders,
299+
$email->placeholders
300+
);
301+
}
207302
}
303+

includes/wcs-cart-functions.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function wcs_cart_totals_shipping_html() {
108108
<?php wcs_cart_print_shipping_input( $recurring_cart_package_key, $shipping_method ); ?>
109109
<?php do_action( 'woocommerce_after_shipping_rate', $shipping_method, $recurring_cart_package_key ); ?>
110110
<?php if ( ! empty( $show_package_details ) ) : ?>
111-
<?php echo '<p class="woocommerce-shipping-contents"><small>' . esc_html( $package_details ) . '</small></p>'; ?>
111+
<?php echo '<p class="woocommerce-shipping-contents"><small>' . wp_kses_post( $package_details ) . '</small></p>'; ?>
112112
<?php endif; ?>
113113
<?php if ( $recurring_rates_match_initial_rates ) : ?>
114114
<?php wcs_cart_print_inherit_shipping_flag( $recurring_cart_package_key ); ?>

templates/cart/cart-recurring-shipping.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
<?php endif; ?>
3636

3737
<?php if ( $show_package_details ) : ?>
38-
<?php echo '<p class="woocommerce-shipping-contents"><small>' . esc_html( $package_details ) . '</small></p>'; ?>
38+
<?php echo '<p class="woocommerce-shipping-contents"><small>' . wp_kses_post( $package_details ) . '</small></p>'; ?>
3939
<?php endif; ?>
4040
</td>
4141
</tr>

0 commit comments

Comments
 (0)