Updates to 8.2.0

This commit is contained in:
WooCommerce 2025-12-11 10:21:12 +00:00
parent dda5b19e32
commit c7e30d354e
44 changed files with 573 additions and 167 deletions

View File

@ -3,10 +3,10 @@ body.wcs-modal-open {
}
.wcs-modal {
display: none;
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
height: 0;
@ -17,6 +17,7 @@ body.wcs-modal-open {
}
.wcs-modal.open {
display: flex;
position: fixed;
width: 100%;
height: 100vh;
@ -50,6 +51,10 @@ body.wcs-modal-open {
top: 0;
right: 0;
z-index: 50;
border: none;
background: transparent;
padding: 0;
cursor: pointer;
}
.wcs-modal .content-wrapper .modal-header {

View File

@ -15,6 +15,17 @@
margin-bottom: 0.5em;
}
button.subscription-auto-renew-toggle,
button.subscription-auto-renew-toggle:hover {
background: none;
border: none;
padding: 0;
cursor: pointer;
font: inherit;
box-shadow: none;
text-shadow: none;
}
.subscription-auto-renew-toggle {
margin-left: 5px;
margin-bottom: 2px;

View File

@ -11,8 +11,11 @@ jQuery( function ( $ ) {
var $early_renewal_modal_content = $( '.wcs-modal > .content-wrapper' );
function getTxtColor() {
if ( ! txtColor && $icon && $icon.length ) {
txtColor = getComputedStyle( $icon[ 0 ] ).color;
if ( ! txtColor ) {
// Create a temporary link to get the theme's accent color.
var $tempLink = $( '<a href="#" style="display:none;"></a>' ).appendTo( $toggleContainer );
txtColor = getComputedStyle( $tempLink[ 0 ] ).color;
$tempLink.remove();
}
return txtColor;
@ -38,9 +41,6 @@ jQuery( function ( $ ) {
function onToggle( e ) {
e.preventDefault();
// Remove focus from the toggle element.
$toggle.trigger( 'blur' );
// Ignore the request if the toggle is disabled.
if ( $toggle.hasClass( 'subscription-auto-renew-toggle--disabled' ) ) {
return;
@ -105,17 +105,20 @@ jQuery( function ( $ ) {
$icon.removeClass( 'fa-toggle-off' ).addClass( 'fa-toggle-on' );
$toggle
.removeClass( 'subscription-auto-renew-toggle--off' )
.addClass( 'subscription-auto-renew-toggle--on' );
.addClass( 'subscription-auto-renew-toggle--on' )
.attr( 'aria-checked', 'true' );
}
function displayToggleOff() {
$icon.removeClass( 'fa-toggle-on' ).addClass( 'fa-toggle-off' );
$toggle
.removeClass( 'subscription-auto-renew-toggle--on' )
.addClass( 'subscription-auto-renew-toggle--off' );
.addClass( 'subscription-auto-renew-toggle--off' )
.attr( 'aria-checked', 'false' );
}
function blockToggle() {
$toggle.addClass( 'subscription-auto-renew-toggle--disabled' );
$toggleContainer.block( {
message: null,
overlayCSS: { opacity: 0.0 },
@ -123,6 +126,7 @@ jQuery( function ( $ ) {
}
function unblockToggle() {
$toggle.removeClass( 'subscription-auto-renew-toggle--disabled' );
$toggleContainer.unblock();
}

View File

@ -1,11 +1,13 @@
jQuery( function ( $ ) {
const modals = $( '.wcs-modal' );
const $modals = $( '.wcs-modal' );
let $currentModal;
let $triggerElement;
// Resize all open modals on window resize.
$( window ).on( 'resize', resizeModals );
// Initialize modals
$( modals ).each( function () {
$( $modals ).each( function () {
trigger = $( this ).data( 'modal-trigger' );
$( trigger ).on( 'click', { modal: this }, show_modal );
} );
@ -18,34 +20,35 @@ jQuery( function ( $ ) {
* @param {JQuery event} event
*/
function show_modal( event ) {
const modal = $( event.data.modal );
$triggerElement = $( event.target );
$currentModal = $( event.data.modal );
if ( ! should_show_modal( modal ) ) {
if ( ! should_show_modal( $currentModal ) ) {
return;
}
// Prevent the trigger element event being triggered.
event.preventDefault();
const contentWrapper = modal.find( '.content-wrapper' );
const close = modal.find( '.close' );
const $contentWrapper = $currentModal.find( '.content-wrapper' );
const $close = $currentModal.find( '.close' );
modal.trigger( 'focus' );
modal.addClass( 'open' );
resizeModal( modal );
$currentModal.addClass( 'open' );
resizeModal( $currentModal );
$( document.body ).toggleClass( 'wcs-modal-open', true );
$currentModal.focus();
document.addEventListener( 'focusin', keepFocusInModal );
// Attach callbacks to handle closing the modal.
close.on( 'click', () => close_modal( modal ) );
modal.on( 'click', () => close_modal( modal ) );
contentWrapper.on( 'click', ( e ) => e.stopPropagation() );
$close.on( 'click', () => close_modal( $currentModal ) );
$currentModal.on( 'click', () => close_modal( $currentModal ) );
$contentWrapper.on( 'click', ( e ) => e.stopPropagation() );
// Close the modal if the escape key is pressed.
modal.on( 'keyup', function ( e ) {
$currentModal.on( 'keyup', function ( e ) {
if ( 27 === e.keyCode ) {
close_modal( modal );
close_modal( $currentModal );
}
} );
}
@ -53,14 +56,17 @@ jQuery( function ( $ ) {
/**
* Closes a modal and resets any forced height styles.
*
* @param {JQuery Object} modal
* @param {JQuery Object} $modal
*/
function close_modal( modal ) {
modal.removeClass( 'open' );
$( modal ).find( '.content-wrapper' ).css( 'height', '' );
function close_modal( $modal ) {
$modal.removeClass( 'open' );
$( $modal ).find( '.content-wrapper' ).css( 'height', '' );
if ( 0 === modals.filter( '.open' ).length ) {
if ( 0 === $modals.filter( '.open' ).length ) {
$( document.body ).removeClass( 'wcs-modal-open' );
$currentModal = false;
document.removeEventListener( 'focusin', keepFocusInModal );
$triggerElement.focus();
}
}
@ -86,7 +92,7 @@ jQuery( function ( $ ) {
* Resize all open modals to fit the display.
*/
function resizeModals() {
$( modals ).each( function () {
$( $modals ).each( function () {
if ( ! $( this ).hasClass( 'open' ) ) {
return;
}
@ -98,17 +104,28 @@ jQuery( function ( $ ) {
/**
* Resize a modal to fit the display.
*
* @param {JQuery Object} modal
* @param {JQuery Object} $modal
*/
function resizeModal( modal ) {
var modal_container = $( modal ).find( '.content-wrapper' );
function resizeModal( $modal ) {
const $modal_container = $( $modal ).find( '.content-wrapper' );
// On smaller displays the height is already forced to be 100% in CSS. We just clear any height we might set previously.
if ( $( window ).width() <= 414 ) {
modal_container.css( 'height', '' );
} else if ( modal_container.height() > $( window ).height() ) {
$modal_container.css( 'height', '' );
} else if ( $modal_container.height() > $( window ).height() ) {
// Force the container height to trigger scroll etc if it doesn't fit on the screen.
modal_container.css( 'height', '90%' );
$modal_container.css( 'height', '90%' );
}
}
/**
* If focus moves out of the open modal, return focus to it.
*
* @param event
*/
function keepFocusInModal( event ) {
if ( $currentModal && ! $currentModal[0].contains( event.target ) ) {
$currentModal.focus();
}
}
} );

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-blocks-data-store', 'wc-price-format', 'wc-settings', 'wp-data', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => 'ac38e67791839651a41a');
<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-blocks-data-store', 'wc-price-format', 'wc-settings', 'wp-data', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '841d6ab2fe0080f15a21');

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,30 @@
*** WooCommerce Subscriptions Changelog ***
2025-12-10 - version 8.2.0
* Fix: Various accessibility issues.
* Added button roles.
* Added labels to remove product links.
* Adding missing header labels on related orders table.
* Prevented various hidden elements from being exposed to screen readers.
* Fixed focus problems for the renewal dialog.
* Fixed focus when closing modals.
* Fixed toggle state change using spacebar.
* Fixed label for sign up now button.
* Fixed toggle aria-label text to reflect state.
* Added aria-haspopup attribute to buttons that open modals.
* Fixed link for view order aria-label.
* Fixed renew now button text and aria-label.
* Added aria-modal and role attributes to modals and dialogs.
* Fix: Resubscribe button now appears correctly for cancelled limited subscriptions.
* Fix: Missing translation for pricing string on languages without plural form.
* Fix: Customized subscriptions links in WooCommerce emails now work correctly.
* Fix: Possible fatal errors when switching grouped subscriptions.
* Fix: Prevent unnecessary log entries related to gifted subscriptions.
* Fix: Make it easier for translation plugins to respect individual customer language preferences when sending subscription emails.
* Fix: Prevent recurring local pickup options from displaying for virtual subscriptions when the cart contains physical products.
* Fix: Prevent shipping summary from displaying on Blocks Checkout for virtual subscriptions.
* Fix: Item price in blocks checkout is correctly displayed without "due today" words for items with zero sign up fee.
2025-11-13 - version 8.1.0
* Fix: Prevent a fatal error that can occur when previewing emails in WooCommerce email settings.
* Fix: Prevent technical subscription-specific discount types from appearing in the coupon edit UI.

View File

@ -45,6 +45,7 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_show_welcome_message' ) );
add_action( 'plugins_loaded', array( $this, 'init_gifting' ) );
add_action( 'plugins_loaded', array( $this, 'init_downloads' ) );
add_action( 'admin_notices', array( WC_Subscription_Downloads_Settings::class, 'add_notice_about_bundled_feature' ) );
}
/**

View File

@ -76,6 +76,32 @@ class WC_Product_Subscription extends WC_Product_Simple {
return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this );
}
/**
* Provides the descriptive text for add-to-cart buttons.
*
* @return mixed
*/
public function add_to_cart_description() {
if ( $this->is_purchasable() && $this->is_in_stock() ) {
// For accessibility reasons it is recommended that the aria-label is the same as, or else starts with, the
// same text that is visible on the button itself.
$text = sprintf(
// Translators: %1$s: Pre-determined add-to-cart text 2: Product title.
_x( '%1$s: &ldquo;%2$s&rdquo;', 'Add-to-cart button description', 'woocommerce-subscriptions' ),
WC_Subscriptions_Product::get_add_to_cart_text(),
$this->get_name()
);
} else {
$text = sprintf(
// Translators: %1$s: Product title.
__( 'Read more about &ldquo;%1$s&rdquo;', 'woocommerce-subscriptions' ),
$this->get_name()
);
}
return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this );
}
/**
* Get the add to cart button text for the single page
*

View File

@ -428,13 +428,38 @@ class WC_Subscription extends WC_Order {
/**
* Checks if the subscription contains an unavailable product.
*
* A product is considered unavailable if it is:
* - Deleted (not found)
* - Not published (draft, trash, private, etc.)
*
* Note: This method intentionally does NOT use is_purchasable() to avoid incorrectly
* flagging limited products as unavailable. Limited products return is_purchasable() = false
* for users with existing subscriptions, but they should still be available for resubscribe.
* Functions like wcs_can_user_resubscribe_to() have specific logic to handle limited products
* by checking if the user has an active subscription.
*
* @return bool
*/
public function contains_unavailable_product() {
/** @var WC_Order_Item_Product $line_item */
foreach ( $this->get_items() as $line_item ) {
$product = $line_item->get_product();
if ( ! $product instanceof WC_Product || ! $product->is_purchasable() ) {
// Product doesn't exist (deleted).
if ( ! $product instanceof WC_Product ) {
return true;
}
// If the product is a subscription variation, use the parent product.
if ( $product->is_type( 'subscription_variation' ) ) {
$parent_product_id = $product->get_parent_id();
$product = wc_get_product( $parent_product_id );
}
// Check if product is published. Products with other statuses (draft, trash, private)
// are not available for purchase or resubscribe.
$product_status = $product->get_status();
if ( 'publish' !== $product_status ) {
return true;
}
}

View File

@ -64,6 +64,7 @@ class WC_Subscriptions_Addresses {
$actions['change_address'] = array(
'url' => esc_url( add_query_arg( array( 'subscription' => $subscription->get_id() ), wc_get_endpoint_url( 'edit-address', 'shipping' ) ) ),
'name' => __( 'Change address', 'woocommerce-subscriptions' ),
'role' => 'link',
);
}

View File

@ -383,10 +383,17 @@ class WC_Subscriptions_Product {
}
break;
}
} elseif ( 1 === $billing_interval ) {
$subscription_string = sprintf(
// translators: 1$: recurring amount, 2$: subscription period (e.g. "month") (e.g. "$15 / month").
__( '%1$s / %2$s', 'woocommerce-subscriptions' ),
$price,
wcs_get_subscription_period_strings( $billing_interval, $billing_period )
);
} else {
$subscription_string = sprintf(
// translators: 1$: recurring amount, 2$: subscription period (e.g. "month" or "3 months") (e.g. "$15 / month" or "$15 every 2nd month").
_n( '%1$s / %2$s', '%1$s every %2$s', $billing_interval, 'woocommerce-subscriptions' ),
// translators: 1$: recurring amount, 2$: subscription period (e.g. "3 months") (e.g. "$15 every 2nd month").
__( '%1$s every %2$s', 'woocommerce-subscriptions' ),
$price,
wcs_get_subscription_period_strings( $billing_interval, $billing_period )
);

View File

@ -51,6 +51,13 @@ class WCS_Modal {
*/
private $actions = array();
/**
* A unique ID for the modal to use in HTML ID attribute.
*
* @var string
*/
private $id = '';
/**
* Registers the scripts and stylesheets needed to display the modals.
*
@ -104,6 +111,7 @@ class WCS_Modal {
$this->trigger = $trigger;
$this->heading = $heading;
$this->actions = $actions;
$this->id = wp_unique_id( 'wcs-modal-' );
// Allow callers to provide the callback without any parameters. Assuming the content provided is the callback.
if ( 'callback' === $this->content_type && ! isset( $content['parameters'] ) ) {
@ -229,6 +237,30 @@ class WCS_Modal {
return $this->trigger;
}
/**
* Sets the modal's unique ID.
*
* This is used for the actual HTML ID attribute, and so should follow the normal CSS identifier rules.
*
* @since 8.2.0
*
* @param string $id The modal's unique ID.
*/
public function set_id( $id ) {
$this->id = $id;
}
/**
* Returns the modal's unique ID.
*
* @since 8.2.0
*
* @return string The modal's unique ID.
*/
public function get_id() {
return $this->id;
}
/**
* Returns a flattened string of HTML element attributes from an array of attributes and values.
*

View File

@ -13,12 +13,13 @@ class WCS_Query extends WC_Query {
add_filter( 'the_title', array( $this, 'change_endpoint_title' ), 11, 1 );
add_filter( 'woocommerce_get_query_vars', array( $this, 'add_wcs_query_vars' ) );
if ( ! is_admin() ) {
add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
add_action( 'parse_request', array( $this, 'parse_request' ), 0 );
add_action( 'pre_get_posts', array( $this, 'maybe_redirect_payment_methods' ) );
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 11 );
add_filter( 'woocommerce_get_query_vars', array( $this, 'add_wcs_query_vars' ) );
// Inserting your new tab/page into the My Account page.
add_filter( 'woocommerce_account_menu_items', array( $this, 'add_menu_items' ) );

View File

@ -21,8 +21,7 @@ class WCS_Email_Customer_On_Hold_Renewal_Order extends WC_Email_Customer_On_Hold
$this->customer_email = true;
$this->title = __( 'On-hold Renewal Order', 'woocommerce-subscriptions' );
$this->description = __( 'This is an order notification sent to customers containing order details after a renewal order is placed on-hold.', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {site_title} renewal order has been received!', 'woocommerce-subscriptions' );
$this->heading = __( 'Thank you for your renewal order', 'woocommerce-subscriptions' );
$this->template_html = 'emails/customer-on-hold-renewal-order.php';
$this->template_plain = 'emails/plain/customer-on-hold-renewal-order.php';
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
@ -47,7 +46,7 @@ class WCS_Email_Customer_On_Hold_Renewal_Order extends WC_Email_Customer_On_Hold
* @return string
*/
public function get_default_subject() {
return $this->subject;
return __( 'Your {site_title} renewal order has been received!', 'woocommerce-subscriptions' );
}
/**
@ -57,7 +56,7 @@ class WCS_Email_Customer_On_Hold_Renewal_Order extends WC_Email_Customer_On_Hold
* @return string
*/
public function get_default_heading() {
return $this->heading;
return __( 'Thank you for your renewal order', 'woocommerce-subscriptions' );
}
/**

View File

@ -44,9 +44,6 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
$this->template_plain = 'emails/plain/customer-renewal-invoice.php';
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
$this->subject = __( 'Invoice for renewal order {order_number} from {order_date}', 'woocommerce-subscriptions' );
$this->heading = __( 'Invoice for renewal order {order_number}', 'woocommerce-subscriptions' );
// Triggers for this email
add_action( 'woocommerce_generated_manual_renewal_order_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_renewal_notification', array( $this, 'trigger' ) );
@ -63,7 +60,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
* @return string
*/
public function get_default_subject( $paid = false ) {
return $this->subject;
return __( 'Invoice for renewal order {order_number} from {order_date}', 'woocommerce-subscriptions' );
}
/**
@ -74,7 +71,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
* @return string
*/
public function get_default_heading( $paid = false ) {
return $this->heading;
return __( 'Invoice for renewal order {order_number}', 'woocommerce-subscriptions' );
}
/**

View File

@ -24,9 +24,6 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
$this->description = __( 'This is an order notification sent to the customer after payment for a subscription renewal order is completed. It contains the renewal order details.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'Thank you for your order', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {site_title} renewal order receipt from {order_date}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/customer-processing-renewal-order.php';
$this->template_plain = 'emails/plain/customer-processing-renewal-order.php';
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
@ -48,7 +45,7 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
* @return string
*/
public function get_default_subject() {
return $this->subject;
return __( 'Your {site_title} renewal order receipt from {order_date}', 'woocommerce-subscriptions' );
}
/**
@ -58,7 +55,7 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
* @return string
*/
public function get_default_heading() {
return $this->heading;
return __( 'Thank you for your order', 'woocommerce-subscriptions' );
}
/**

View File

@ -190,12 +190,22 @@ function wcs_price_string( $subscription_details ) {
break;
}
} elseif ( ! empty( $subscription_details['initial_amount'] ) ) {
// translators: 1$: initial amount, 2$: initial description (e.g. "up front"), 3$: recurring amount, 4$: subscription period (e.g. "month" or "3 months")
$subscription_string = sprintf( _n( '%1$s %2$s then %3$s / %4$s', '%1$s %2$s then %3$s every %4$s', $subscription_details['subscription_interval'], 'woocommerce-subscriptions' ), $initial_amount_string, $subscription_details['initial_description'], $recurring_amount_string, $subscription_period_string );
if ( 1 === $subscription_details['subscription_interval'] ) {
// translators: 1$: initial amount, 2$: initial description (e.g. "up front"), 3$: recurring amount, 4$: subscription period (e.g. "month")
$subscription_string = sprintf( __( '%1$s %2$s then %3$s / %4$s', 'woocommerce-subscriptions' ), $initial_amount_string, $subscription_details['initial_description'], $recurring_amount_string, $subscription_period_string );
} else {
// translators: 1$: initial amount, 2$: initial description (e.g. "up front"), 3$: recurring amount, 4$: subscription period (e.g. "3 months")
$subscription_string = sprintf( __( '%1$s %2$s then %3$s every %4$s', 'woocommerce-subscriptions' ), $initial_amount_string, $subscription_details['initial_description'], $recurring_amount_string, $subscription_period_string );
}
} elseif ( ! empty( $subscription_details['recurring_amount'] ) || intval( $subscription_details['recurring_amount'] ) === 0 ) {
if ( true === $subscription_details['use_per_slash'] ) {
// translators: 1$: recurring amount, 2$: subscription period (e.g. "month" or "3 months") (e.g. "$15 / month" or "$15 every 2nd month")
$subscription_string = sprintf( _n( '%1$s / %2$s', '%1$s every %2$s', $subscription_details['subscription_interval'], 'woocommerce-subscriptions' ), $recurring_amount_string, $subscription_period_string );
if ( 1 === $subscription_details['subscription_interval'] ) {
// translators: 1$: recurring amount, 2$: subscription period (e.g. "month") (e.g. "$15 / month")
$subscription_string = sprintf( __( '%1$s / %2$s', 'woocommerce-subscriptions' ), $recurring_amount_string, $subscription_period_string );
} else {
// translators: 1$: recurring amount, 2$: subscription period (e.g. "3 months") (e.g. "$15 every 2nd month")
$subscription_string = sprintf( __( '%1$s every %2$s', 'woocommerce-subscriptions' ), $recurring_amount_string, $subscription_period_string );
}
} else {
// translators: %1$: recurring amount (e.g. "$15"), %2$: subscription period (e.g. "month") (e.g. "$15 every 2nd month")
$subscription_string = sprintf( __( '%1$s every %2$s', 'woocommerce-subscriptions' ), $recurring_amount_string, $subscription_period_string );

View File

@ -310,6 +310,7 @@ function wcs_get_all_user_actions_for_subscription( $subscription, $user_id ) {
'url' => wcs_get_users_change_status_link( $subscription->get_id(), 'active', $current_status ),
'name' => __( 'Reactivate', 'woocommerce-subscriptions' ),
'block_ui' => true,
'role' => 'button',
);
}
@ -318,6 +319,7 @@ function wcs_get_all_user_actions_for_subscription( $subscription, $user_id ) {
'url' => wcs_get_users_resubscribe_link( $subscription ),
'name' => __( 'Resubscribe', 'woocommerce-subscriptions' ),
'block_ui' => true,
'role' => 'button',
);
}
@ -328,6 +330,7 @@ function wcs_get_all_user_actions_for_subscription( $subscription, $user_id ) {
'url' => wcs_get_users_change_status_link( $subscription->get_id(), 'cancelled', $current_status ),
'name' => _x( 'Cancel', 'an action on a subscription', 'woocommerce-subscriptions' ),
'block_ui' => true,
'role' => 'button',
);
}
}

View File

@ -68,9 +68,7 @@ class WC_Subscription_Downloads_Products {
<select id="subscription-downloads-ids" multiple="multiple" data-action="wc_subscription_downloads_search" data-placeholder="<?php esc_attr_e( 'Select subscriptions', 'woocommerce-subscriptions' ); ?>" class="subscription-downloads-ids wc-product-search" name="_subscription_downloads_ids[]" style="width: 50%;">
<?php
$subscriptions_ids = WC_Subscription_Downloads::get_subscriptions( $post->ID );
if ( empty( $subscriptions_ids ) ) {
$subscriptions_ids = get_post_meta( $post->ID, '_subscription_downloads_ids', true );
}
if ( $subscriptions_ids ) {
foreach ( $subscriptions_ids as $subscription_id ) {
$_subscription = wc_get_product( $subscription_id );
@ -202,11 +200,8 @@ class WC_Subscription_Downloads_Products {
protected function update_subscription_downloads( $product_id, $subscriptions ) {
global $wpdb;
if ( version_compare( WC_VERSION, '3.0', '<' ) && ! empty( $subscriptions ) ) {
$subscriptions = explode( ',', $subscriptions );
}
$current = WC_Subscription_Downloads::get_subscriptions( $product_id );
$subscriptions = (array) $subscriptions;
$current = WC_Subscription_Downloads::get_subscriptions( $product_id );
// Delete items.
$delete_ids = array_diff( $current, $subscriptions );
@ -227,7 +222,7 @@ class WC_Subscription_Downloads_Products {
$_orders = $this->get_orders( $delete );
foreach ( $_orders as $order_id ) {
$_product = wc_get_product( $product_id );
$downloads = version_compare( WC_VERSION, '3.0', '<' ) ? $_product->get_files() : $_product->get_downloads();
$downloads = $_product->get_downloads();
// Adds the downloadable files to the order/subscription.
foreach ( array_keys( $downloads ) as $download_id ) {
@ -265,7 +260,7 @@ class WC_Subscription_Downloads_Products {
}
$_product = wc_get_product( $product_id );
$downloads = version_compare( WC_VERSION, '3.0', '<' ) ? $_product->get_files() : $_product->get_downloads();
$downloads = $_product->get_downloads();
// Adds the downloadable files to the order/subscription.
foreach ( array_keys( $downloads ) as $download_id ) {
@ -284,17 +279,14 @@ class WC_Subscription_Downloads_Products {
* @return void
*/
public function save_simple_product_data( $product_id ) {
$subscription_ids = ! empty( $_POST['_subscription_downloads_ids'] ) ? wc_clean( wp_unslash( $_POST['_subscription_downloads_ids'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_downloads_ids = ! empty( $_POST['_subscription_downloads_ids'] ) ? wc_clean( wp_unslash( $_POST['_subscription_downloads_ids'] ) ) : '';
if ( ! isset( $_POST['_downloadable'] ) || 'publish' !== get_post_status( $product_id ) ) {
update_post_meta( $product_id, '_subscription_downloads_ids', $subscription_ids );
return;
if ( empty( $subscription_downloads_ids ) ) {
$subscription_downloads_ids = array();
}
delete_post_meta( $product_id, '_subscription_downloads_ids', $subscription_ids );
$subscriptions = $subscription_ids ?: array();
$this->update_subscription_downloads( $product_id, $subscriptions );
$this->update_subscription_downloads( $product_id, $subscription_downloads_ids );
}
/**
@ -310,15 +302,11 @@ class WC_Subscription_Downloads_Products {
return;
}
$subscriptions = isset( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ? wc_clean( wp_unslash( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ) : array();
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_download_ids = isset( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ? wc_clean( wp_unslash( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ) : array();
$subscription_download_ids = array_filter( $subscription_download_ids ); // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- $subscription_download_ids are already passed through wc_clean() and wp_unslash().
if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
$subscriptions = explode( ',', $subscriptions );
}
$subscriptions = array_filter( $subscriptions ); // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- $subscriptions are already passed through wc_clean() and wp_unslash().
$this->update_subscription_downloads( $variation_id, $subscriptions );
$this->update_subscription_downloads( $variation_id, $subscription_download_ids );
}
/**

View File

@ -1,5 +1,7 @@
<?php
use Automattic\Jetpack\Constants;
/**
* Registers and manages settings related to linked downloadable files functionality.
*
@ -10,6 +12,39 @@ class WC_Subscription_Downloads_Settings {
add_filter( 'woocommerce_subscription_settings', array( $this, 'add_settings' ) );
}
/**
* Check if WooCommerce Subscription Downloads plugin is enabled and add a warning about the bundled feature if it is.
*
* @since 8.0.0
*/
public static function add_notice_about_bundled_feature() {
$screen = get_current_screen();
if ( ! $screen ) {
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$is_subscriptions_settings_page = 'woocommerce_page_wc-settings' === $screen->id && isset( $_GET['tab'] ) && 'subscriptions' === sanitize_text_field( wp_unslash( $_GET['tab'] ) );
// Only show notice on plugins page or subscriptions settings page.
if ( 'plugins' !== $screen->id && ! $is_subscriptions_settings_page ) {
return;
}
if ( Constants::get_constant( 'WC_SUBSCRIPTION_DOWNLOADS_VERSION' ) ) {
$message = __( 'WooCommerce Subscription Downloads is now part of WooCommerce Subscriptions — no extra plugin needed. You can deactivate and uninstall WooCommerce Subscription Downloads via the plugin admin screen.', 'woocommerce-subscriptions' );
wp_admin_notice(
$message,
array(
'type' => 'warning',
'dismissible' => true,
)
);
}
}
/**
* Adds our settings to the main subscription settings page.
*

View File

@ -51,7 +51,7 @@ class WC_Subscription_Downloads {
}
/**
* Get subscriptions from a downloadable product.
* Given the ID of a downloadable product, returns an array of linked subscription product IDs.
*
* @param int $product_id
*

View File

@ -69,10 +69,20 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal {
if ( wcs_can_user_renew_early( $subscription ) && $subscription->payment_method_supports( 'subscription_date_changes' ) && $subscription->has_status( 'active' ) ) {
$actions['subscription_renewal_early'] = array(
$action = array(
'url' => wcs_get_early_renewal_url( $subscription ),
'name' => __( 'Renew now', 'woocommerce-subscriptions' ),
'role' => 'link',
);
// Set role to 'button' if renewal via modal is enabled (it opens a modal).
// Modal ID is set to a predictable value containing the subscription ID for aria-controls.
if ( WCS_Early_Renewal_Manager::is_early_renewal_via_modal_enabled() ) {
$action['role'] = 'button';
$action['modal_id'] = 'wcs-early-renewal-modal-' . $subscription->get_id();
}
$actions['subscription_renewal_early'] = $action;
}
return $actions;

View File

@ -51,6 +51,7 @@ class WCS_Early_Renewal_Modal_Handler {
if ( wc_wp_theme_get_element_class_name( 'button' ) ) {
$place_order_action['attributes']['class'] .= ' ' . wc_wp_theme_get_element_class_name( 'button' );
$place_order_action['attributes']['role'] = 'button';
}
$callback_args = array(
@ -59,6 +60,8 @@ class WCS_Early_Renewal_Modal_Handler {
);
$modal = new WCS_Modal( $callback_args, '.subscription_renewal_early', 'callback', __( 'Renew early', 'woocommerce-subscriptions' ) );
// Set the modal ID to match the predictable value used for aria-controls in subscription-details.php
$modal->set_id( 'wcs-early-renewal-modal-' . $subscription->get_id() );
$modal->add_action( $place_order_action );
$modal->print_html();
}

View File

@ -517,7 +517,7 @@ class WCS_Gifting {
public static function is_gifted_subscription( $subscription ) {
$is_gifted_subscription = false;
if ( ! $subscription instanceof WC_Subscription ) {
if ( is_int( $subscription ) ) {
$subscription = wcs_get_subscription( $subscription );
}

View File

@ -30,8 +30,6 @@ class WCSG_Email_Completed_Renewal_Order extends WCS_Email_Completed_Renewal_Ord
$this->title = __( 'Completed Renewal Order - Recipient', 'woocommerce-subscriptions' );
$this->description = __( 'Renewal order complete emails are sent to the recipient when a subscription renewal order is marked complete and usually indicates that the item for that renewal period has been shipped.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'Your renewal order is complete', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {blogname} renewal order from {order_date} is complete', 'woocommerce-subscriptions' );
$this->template_html = 'emails/recipient-completed-renewal-order.php';
$this->template_plain = 'emails/plain/recipient-completed-renewal-order.php';
@ -42,6 +40,28 @@ class WCSG_Email_Completed_Renewal_Order extends WCS_Email_Completed_Renewal_Ord
WC_Email::__construct();
}
/**
* Get the default e-mail subject.
*
* @param bool $paid Whether the order has been paid or not.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.5.3
* @return string
*/
public function get_default_subject( $paid = false ) {
return __( 'Your {blogname} renewal order from {order_date} is complete', 'woocommerce-subscriptions' );
}
/**
* Get the default e-mail heading.
*
* @param bool $paid Whether the order has been paid or not.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.5.3
* @return string
*/
public function get_default_heading( $paid = false ) {
return __( 'Your renewal order is complete', 'woocommerce-subscriptions' );
}
/**
* Trigger function.
*

View File

@ -59,8 +59,7 @@ class WCSG_Email_Customer_New_Account extends WC_Email {
$this->title = __( 'New Recipient Account', 'woocommerce-subscriptions' );
$this->description = __( 'New account notification emails are sent to the subscription recipient when an account is created for them.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->subject = __( 'Your account on {site_title}', 'woocommerce-subscriptions' );
$this->heading = __( 'Welcome to {site_title}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/new-recipient-customer.php';
$this->template_plain = 'emails/plain/new-recipient-customer.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/';
@ -71,6 +70,29 @@ class WCSG_Email_Customer_New_Account extends WC_Email {
WC_Email::__construct();
}
/**
* Get the default e-mail subject.
*
* @param bool $paid Whether the order has been paid or not.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.5.3
* @return string
*/
public function get_default_subject( $paid = false ) {
return __( 'Your account on {site_title}', 'woocommerce-subscriptions' );
}
/**
* Get the default e-mail heading.
*
* @param bool $paid Whether the order has been paid or not.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.5.3
* @return string
*/
public function get_default_heading( $paid = false ) {
return __( 'Welcome to {site_title}', 'woocommerce-subscriptions' );
}
/**
* Trigger function.
*

View File

@ -30,8 +30,7 @@ class WCSG_Email_Processing_Renewal_Order extends WCS_Email_Processing_Renewal_O
$this->title = __( 'Processing Renewal Order - Recipient', 'woocommerce-subscriptions' );
$this->description = __( 'This is an order notification sent to the recipient after payment for a subscription renewal order is completed. It contains the renewal order details.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'Thank you for your order', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {blogname} renewal order receipt from {order_date}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/recipient-processing-renewal-order.php';
$this->template_plain = 'emails/plain/recipient-processing-renewal-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/';
@ -42,6 +41,29 @@ class WCSG_Email_Processing_Renewal_Order extends WCS_Email_Processing_Renewal_O
WC_Email::__construct();
}
/**
* Get the default e-mail subject.
*
* @param bool $paid Whether the order has been paid or not.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.5.3
* @return string
*/
public function get_default_subject( $paid = false ) {
return __( 'Your {blogname} renewal order receipt from {order_date}', 'woocommerce-subscriptions' );
}
/**
* Get the default e-mail heading.
*
* @param bool $paid Whether the order has been paid or not.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.5.3
* @return string
*/
public function get_default_heading( $paid = false ) {
return __( 'Thank you for your order', 'woocommerce-subscriptions' );
}
/**
* Trigger function.
*

View File

@ -51,8 +51,7 @@ class WCSG_Email_Recipient_New_Initial_Order extends WC_Email {
$this->title = __( 'New Initial Order - Recipient', 'woocommerce-subscriptions' );
$this->description = __( 'This email is sent to recipients notifying them of subscriptions purchased for them.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'New Order', 'woocommerce-subscriptions' );
$this->subject = __( 'Your new subscriptions at {site_title}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/recipient-new-initial-order.php';
$this->template_plain = 'emails/plain/recipient-new-initial-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/';
@ -63,6 +62,28 @@ class WCSG_Email_Recipient_New_Initial_Order extends WC_Email {
WC_Email::__construct();
}
/**
* Get the default e-mail subject.
*
* @param bool $paid Whether the order has been paid or not.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.5.3
* @return string
*/
public function get_default_subject( $paid = false ) {
return __( 'Your new subscriptions at {site_title}', 'woocommerce-subscriptions' );
}
/**
* Get the default e-mail heading.
*
* @param bool $paid Whether the order has been paid or not.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.5.3
* @return string
*/
public function get_default_heading( $paid = false ) {
return __( 'New Order', 'woocommerce-subscriptions' );
}
/**
* Trigger function.
*

View File

@ -35,9 +35,6 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
$this->template_plain = 'emails/plain/customer-payment-retry.php';
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
$this->subject = __( 'Automatic payment failed for {order_number}, we will retry {retry_time}', 'woocommerce-subscriptions' );
$this->heading = __( 'Automatic payment failed for order {order_number}', 'woocommerce-subscriptions' );
// We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor
WC_Email::__construct();
}
@ -50,7 +47,7 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
* @return string
*/
public function get_default_subject( $paid = false ) {
return $this->subject;
return __( 'Automatic payment failed for {order_number}, we will retry {retry_time}', 'woocommerce-subscriptions' );
}
/**
@ -61,7 +58,7 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
* @return string
*/
public function get_default_heading( $paid = false ) {
return $this->heading;
return __( 'Automatic payment failed for order {order_number}', 'woocommerce-subscriptions' );
}
/**

View File

@ -32,9 +32,6 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
$this->title = __( 'Payment Retry', 'woocommerce-subscriptions' );
$this->description = __( 'Payment retry emails are sent to chosen recipient(s) when an attempt to automatically process a subscription renewal payment has failed and a retry rule has been applied to retry the payment in the future.', 'woocommerce-subscriptions' );
$this->heading = __( 'Automatic renewal payment failed', 'woocommerce-subscriptions' );
$this->subject = __( '[{site_title}] Automatic payment failed for {order_number}, retry scheduled to run {retry_time}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/admin-payment-retry.php';
$this->template_plain = 'emails/plain/admin-payment-retry.php';
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
@ -53,7 +50,7 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
* @return string
*/
public function get_default_subject() {
return $this->subject;
return __( '[{site_title}] Automatic payment failed for {order_number}, retry scheduled to run {retry_time}', 'woocommerce-subscriptions' );
}
/**
@ -63,7 +60,7 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
* @return string
*/
public function get_default_heading() {
return $this->heading;
return __( 'Automatic renewal payment failed', 'woocommerce-subscriptions' );
}
/**

View File

@ -1282,7 +1282,33 @@ class WC_Subscriptions_Switcher {
// If the switch is for a grouped product, we need to check the other products grouped with this one
if ( $parent_products ) {
foreach ( $parent_products as $parent_id ) {
$switch_product_ids = array_unique( array_merge( $switch_product_ids, wc_get_product( $parent_id )->get_children() ) );
$parent_product = wc_get_product( $parent_id );
if ( ! $parent_product ) {
wc_get_logger()->error(
'Parent product {parent_id} for switch product {product_id} not found',
array(
'parent_id' => $parent_id,
'product_id' => $product_id,
)
);
continue;
}
$parent_product_children = $parent_product->get_children();
if ( ! is_array( $parent_product_children ) ) {
wc_get_logger()->error(
'Children of parent product {parent_id} for switch product {product_id} is not an array',
array(
'parent_id' => $parent_id,
'product_id' => $product_id,
)
);
continue;
}
$switch_product_ids = array_unique( array_merge( $switch_product_ids, $parent_product_children ) );
}
} elseif ( $switch_product->is_type( 'subscription_variation' ) ) {
$switch_product_ids[] = $switch_product->get_parent_id();
@ -1469,7 +1495,33 @@ class WC_Subscriptions_Switcher {
if ( ! empty( $parent_products ) ) {
foreach ( $parent_products as $parent_id ) {
$child_products = array_unique( array_merge( $child_products, wc_get_product( $parent_id )->get_children() ) );
$parent_product = wc_get_product( $parent_id );
if ( ! $parent_product ) {
wc_get_logger()->error(
'Parent product {parent_id} for switch product {product_id} not found',
array(
'parent_id' => $parent_id,
'product_id' => $item['product_id'],
)
);
continue;
}
$parent_product_children = $parent_product->get_children();
if ( ! is_array( $parent_product_children ) ) {
wc_get_logger()->error(
'Children of parent product {parent_id} for switch product {product_id} is not an array',
array(
'parent_id' => $parent_id,
'product_id' => $item['product_id'],
)
);
continue;
}
$child_products = array_unique( array_merge( $child_products, $parent_product_children ) );
}
}

View File

@ -2,14 +2,14 @@
# This file is distributed under the GNU General Public License v3.0.
msgid ""
msgstr ""
"Project-Id-Version: WooCommerce Subscriptions 8.1.0\n"
"Project-Id-Version: WooCommerce Subscriptions 8.2.0\n"
"Report-Msgid-Bugs-To: https://woocommerce.com/contact-us\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2025-11-13T13:50:53+00:00\n"
"POT-Creation-Date: 2025-12-10T19:19:35+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.12.0\n"
"X-Domain: target-repo\n"

View File

@ -20,6 +20,11 @@ class Events {
*/
public function __construct( ?callable $event_recorder = null ) {
$this->event_recorder = $event_recorder ?? function ( string $event_name, array $event_properties = array() ) {
// The WC_Site_Tracking class is not always available, depending on the WC version and request type (e.g. AJAX).
// This check prevents a fatal error if the class is not loaded.
if ( ! class_exists( 'WC_Site_Tracking' ) ) {
return;
}
// Note that the following method ensures nothing is sent home unless tracking is enabled.
WC_Tracks::record_event( $event_name, $event_properties );
};

View File

@ -5,7 +5,7 @@
* Shows the info of a particular subscription without pricing for the recipient on the account page
*
* @package WooCommerce Subscriptions Gifting/Templates
* @version 2.0.0
* @version 8.2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@ -32,13 +32,24 @@ if ( ! defined( 'ABSPATH' ) ) {
foreach ( $subscription_items as $item_id => $item ) {
$_product = apply_filters( 'woocommerce_subscriptions_order_item_product', $item->get_product(), $item );
if ( apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
// Translators: %s: product name.
$aria_label = sprintf( __( 'Remove %s', 'woocommerce-subscriptions' ), esc_html( $_product->get_name() ) );
?>
<tr class="<?php echo esc_attr( apply_filters( 'woocommerce_order_item_class', 'order_item', $item, $subscription ) ); ?>">
<?php if ( $allow_remove_items ) : ?>
<td class="remove_item">
<?php if ( wcs_can_item_be_removed( $item, $subscription ) ) : ?>
<?php $confirm_notice = apply_filters( 'woocommerce_subscriptions_order_item_remove_confirmation_text', __( 'Are you sure you want remove this item from your subscription?', 'woocommerce-subscriptions' ), $item, $_product, $subscription ); ?>
<a href="<?php echo esc_url( WCS_Remove_Item::get_remove_url( $subscription->get_id(), $item_id ) ); ?>" class="remove" onclick="return confirm('<?php printf( esc_html( $confirm_notice ) ); ?>');">&times;</a>
<?php $confirm_notice = apply_filters( 'woocommerce_subscriptions_order_item_remove_confirmation_text', __( 'Are you sure you want to remove this item from your subscription?', 'woocommerce-subscriptions' ), $item, $_product, $subscription ); ?>
<a
href="<?php echo esc_url( WCS_Remove_Item::get_remove_url( $subscription->get_id(), $item_id ) ); ?>"
class="remove"
role="button"
onclick="return confirm('<?php printf( esc_html( $confirm_notice ) ); ?>');"
aria-haspopup="dialog"
aria-label="<?php echo esc_attr( $aria_label ); ?>"
>
&times;
</a>
<?php endif; ?>
</td>
<?php endif; ?>

View File

@ -18,21 +18,34 @@ $include_item_removal_links = $include_switch_links = false;
<?php do_action( 'woocommerce_subscription_totals', $subscription, $include_item_removal_links, $totals, $include_switch_links ); ?>
</div>
<p class="wcs_early_renew_modal_note">
<?php if ( ! empty( $new_next_payment_date ) ) {
echo wp_kses_post( sprintf(
__( 'By renewing your subscription early your next payment will be %s.', 'woocommerce-subscriptions' ),
'<strong>' . esc_html( date_i18n( wc_date_format(), $new_next_payment_date->getOffsetTimestamp() ) ) . '</strong>'
) );
<?php
if ( ! empty( $new_next_payment_date ) ) {
echo wp_kses_post(
sprintf(
// Translators: 1: new next payment date.
__( 'By renewing your subscription early your next payment will be %s.', 'woocommerce-subscriptions' ),
'<strong>' . esc_html( date_i18n( wc_date_format(), $new_next_payment_date->getOffsetTimestamp() ) ) . '</strong>'
)
);
} else {
echo wp_kses_post( sprintf(
__( 'By renewing your subscription early, your scheduled next payment on %s will be cancelled.', 'woocommerce-subscriptions' ),
'<strong>' . esc_html( date_i18n( wc_date_format(), $subscription->get_time( 'next_payment', 'site' ) ) ) . '</strong>'
) );
}?>
echo wp_kses_post(
sprintf(
// Translators: 1: currently schedulednext payment date.
__( 'By renewing your subscription early, your scheduled next payment on %1$s will be cancelled.', 'woocommerce-subscriptions' ),
'<strong>' . esc_html( date_i18n( wc_date_format(), $subscription->get_time( 'next_payment', 'site' ) ) ) . '</strong>'
)
);
}
?>
<br>
<?php echo wp_kses_post( sprintf(
__( 'Want to renew early via the checkout? Click %shere.%s', 'woocommerce-subscriptions' ),
'<a href="' . esc_url( wcs_get_early_renewal_url( $subscription ) ) . '">',
'</a>'
) ) ?>
<?php
echo wp_kses_post(
sprintf(
// Translators: 1: opening link tag 2: closing link tag.
__( '%1$sClick here to renew early via the checkout.%2$s', 'woocommerce-subscriptions' ),
'<a href="' . esc_url( wcs_get_early_renewal_url( $subscription ) ) . '">',
'</a>'
)
)
?>
</p>

View File

@ -9,13 +9,19 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div data-modal-trigger="<?php echo esc_attr( $modal->get_trigger() );?>" class="wcs-modal" tabindex="0">
<article class="content-wrapper">
<div data-modal-trigger="<?php echo esc_attr( $modal->get_trigger() );?>" class="wcs-modal" id="<?php echo esc_attr( $modal->get_id() ); ?>" tabindex="0">
<?php
$article_attributes = 'class="content-wrapper" role="dialog" aria-modal="true"';
if ( $modal->has_heading() ) {
$article_attributes .= ' aria-labelledby="' . esc_attr( $modal->get_id() . '-heading' ) . '"';
}
?>
<article <?php echo $article_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<header class="modal-header">
<?php if ( $modal->has_heading() ) : ?>
<h2><?php echo esc_html( $modal->get_heading() ) ?></h2>
<h2 id="<?php echo esc_attr( $modal->get_id() . '-heading' ); ?>"><?php echo esc_html( $modal->get_heading() ) ?></h2>
<?php endif ?>
<a href="#" onclick="return false;" class="close" style="text-decoration: none;"><span class="dashicons dashicons-no"></span></a>
<button type="button" class="close" aria-label="<?php esc_attr_e( 'Close modal', 'woocommerce-subscriptions' ); ?>"><span class="dashicons dashicons-no"></span></button>
</header>
<div class="content">

View File

@ -22,7 +22,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<th class="subscription-status order-status woocommerce-orders-table__header woocommerce-orders-table__header-order-status woocommerce-orders-table__header-subscription-status"><span class="nobr"><?php esc_html_e( 'Status', 'woocommerce-subscriptions' ); ?></span></th>
<th class="subscription-next-payment order-date woocommerce-orders-table__header woocommerce-orders-table__header-order-date woocommerce-orders-table__header-subscription-next-payment"><span class="nobr"><?php echo esc_html_x( 'Next payment', 'table heading', 'woocommerce-subscriptions' ); ?></span></th>
<th class="subscription-total order-total woocommerce-orders-table__header woocommerce-orders-table__header-order-total woocommerce-orders-table__header-subscription-total"><span class="nobr"><?php echo esc_html_x( 'Total', 'table heading', 'woocommerce-subscriptions' ); ?></span></th>
<th class="subscription-actions order-actions woocommerce-orders-table__header woocommerce-orders-table__header-order-actions woocommerce-orders-table__header-subscription-actions">&nbsp;</th>
<th class="subscription-actions order-actions woocommerce-orders-table__header woocommerce-orders-table__header-order-actions woocommerce-orders-table__header-subscription-actions"><span class="screen-reader-text"><?php esc_html_e( 'Actions', 'woocommerce-subscriptions' ); ?></span></th>
</tr>
</thead>

View File

@ -27,7 +27,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<th class="order-date woocommerce-orders-table__header woocommerce-orders-table__header-order-date woocommerce-orders-table__header-order-date"><span class="nobr"><?php esc_html_e( 'Date', 'woocommerce-subscriptions' ); ?></span></th>
<th class="order-status woocommerce-orders-table__header woocommerce-orders-table__header-order-status"><span class="nobr"><?php esc_html_e( 'Status', 'woocommerce-subscriptions' ); ?></span></th>
<th class="order-total woocommerce-orders-table__header woocommerce-orders-table__header-order-total"><span class="nobr"><?php echo esc_html_x( 'Total', 'table heading', 'woocommerce-subscriptions' ); ?></span></th>
<th class="order-actions woocommerce-orders-table__header woocommerce-orders-table__header-order-actions">&nbsp;</th>
<th class="order-actions woocommerce-orders-table__header woocommerce-orders-table__header-order-actions"><span class="screen-reader-text"><?php esc_html_e( 'Actions', 'woocommerce-subscriptions' ); ?></span></th>
</tr>
</thead>

View File

@ -4,7 +4,7 @@
*
* @author Prospress
* @category WooCommerce Subscriptions/Templates
* @version 7.3.0 - Migrated from WooCommerce Subscriptions v2.6.0
* @version 8.2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@ -26,7 +26,14 @@ if ( ! defined( 'ABSPATH' ) ) {
</tr>
</thead>
<tbody>
<?php foreach ( $subscriptions as $subscription_id => $subscription ) : ?>
<?php
foreach ( $subscriptions as $subscription_id => $subscription ) {
$view_order_label = sprintf(
// Translators: %1$d is the subscription number.
__( 'View subscription %1$d', 'woocommerce-subscriptions' ),
$subscription_id
);
?>
<tr class="order woocommerce-orders-table__row woocommerce-orders-table__row--status-<?php echo esc_attr( $subscription->get_status() ); ?>">
<td class="subscription-id order-number woocommerce-orders-table__cell woocommerce-orders-table__cell-subscription-id woocommerce-orders-table__cell-order-number" data-title="<?php esc_attr_e( 'ID', 'woocommerce-subscriptions' ); ?>">
<?php // translators: placeholder is a subscription number. ?>
@ -44,10 +51,16 @@ if ( ! defined( 'ABSPATH' ) ) {
<?php echo wp_kses_post( $subscription->get_formatted_order_total() ); ?>
</td>
<td class="subscription-actions order-actions woocommerce-orders-table__cell woocommerce-orders-table__cell-subscription-actions woocommerce-orders-table__cell-order-actions">
<a href="<?php echo esc_url( $subscription->get_view_order_url() ); ?>" class="woocommerce-button button view<?php echo esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '' ); ?>"><?php echo esc_html_x( 'View', 'view a subscription', 'woocommerce-subscriptions' ); ?></a>
<a
href="<?php echo esc_url( $subscription->get_view_order_url() ); ?>"
class="woocommerce-button button view<?php echo esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '' ); ?>"
aria-label="<?php echo esc_attr( $view_order_label ); ?>"
>
<?php echo esc_html_x( 'View', 'view a subscription', 'woocommerce-subscriptions' ); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php } // endforeach ?>
</tbody>
</table>

View File

@ -5,7 +5,7 @@
* @author Prospress
* @package WooCommerce_Subscription/Templates
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.19
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.6.5
* @version 8.2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@ -43,22 +43,30 @@ if ( ! defined( 'ABSPATH' ) ) {
<td>
<div class="wcs-auto-renew-toggle">
<?php
$is_auto_renew_on = ! $subscription->is_manual();
$toggle_classes = array( 'subscription-auto-renew-toggle', 'subscription-auto-renew-toggle--hidden' );
$is_duplicate_site = false;
$toggle_classes = array( 'subscription-auto-renew-toggle', 'subscription-auto-renew-toggle--hidden' );
if ( $subscription->is_manual() ) {
$toggle_label = __( 'Enable auto renew', 'woocommerce-subscriptions' );
if ( $is_auto_renew_on ) {
$toggle_classes[] = 'subscription-auto-renew-toggle--on';
} else {
$toggle_classes[] = 'subscription-auto-renew-toggle--off';
if ( WCS_Staging::is_duplicate_site() ) {
$toggle_classes[] = 'subscription-auto-renew-toggle--disabled';
$toggle_classes[] = 'subscription-auto-renew-toggle--disabled';
$is_duplicate_site = true;
}
} else {
$toggle_label = __( 'Disable auto renew', 'woocommerce-subscriptions' );
$toggle_classes[] = 'subscription-auto-renew-toggle--on';
}?>
<a href="#" class="<?php echo esc_attr( implode( ' ' , $toggle_classes ) ); ?>" aria-label="<?php echo esc_attr( $toggle_label ) ?>"><i class="subscription-auto-renew-toggle__i" aria-hidden="true"></i></a>
<?php if ( WCS_Staging::is_duplicate_site() ) : ?>
}
?>
<button
type="button"
role="switch"
aria-checked="<?php echo $is_auto_renew_on ? 'true' : 'false'; ?>"
aria-label="<?php esc_attr_e( 'Auto renew', 'woocommerce-subscriptions' ); ?>"
class="<?php echo esc_attr( implode( ' ', $toggle_classes ) ); ?>"
<?php disabled( $is_duplicate_site ); ?>
><i class="subscription-auto-renew-toggle__i" aria-hidden="true"></i></button>
<?php if ( $is_duplicate_site ) : ?>
<small class="subscription-auto-renew-toggle-disabled-note"><?php echo esc_html__( 'Using the auto-renewal toggle is disabled while in staging mode.', 'woocommerce-subscriptions' ); ?></small>
<?php endif; ?>
</div>
@ -88,10 +96,19 @@ if ( ! defined( 'ABSPATH' ) ) {
if ( wc_wp_theme_get_element_class_name( 'button' ) ) {
$classes[] = wc_wp_theme_get_element_class_name( 'button' );
}
// Role is used for accessibility purposes. Default role is 'button', because of the default visual styling.
$action_role = isset( $action['role'] ) ? $action['role'] : 'button';
?>
<a
href="<?php echo esc_url( $action['url'] ); ?>"
role="<?php echo esc_attr( $action_role ); ?>"
class="<?php echo esc_attr( trim( implode( ' ', $classes ) ) ); ?>"
<?php
if ( isset( $action['modal_id'] ) ) {
echo ' aria-haspopup="dialog" aria-controls="' . esc_attr( $action['modal_id'] ) . '"';
}
?>
>
<?php echo esc_html( $action['name'] ); ?>
</a>
@ -114,8 +131,8 @@ if ( ! defined( 'ABSPATH' ) ) {
<div class="woocommerce-OrderUpdate-description description">
<?php echo wp_kses_post( wpautop( wptexturize( $note->comment_content ) ) ); ?>
</div>
<div class="clear"></div>
</div>
<div class="clear"></div>
</div>
<div class="clear"></div>
</div>
</li>

View File

@ -4,7 +4,7 @@
*
* @package WooCommerce_Subscription/Templates
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.6.0
* @version 7.2.0
* @version 8.2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@ -43,8 +43,21 @@ if ( ! defined( 'ABSPATH' ) ) {
<?php if ( $allow_item_removal ) : ?>
<td class="remove_item">
<?php if ( wcs_can_item_be_removed( $item, $subscription ) ) : ?>
<?php $confirm_notice = apply_filters( 'woocommerce_subscriptions_order_item_remove_confirmation_text', __( 'Are you sure you want to remove this item from your subscription?', 'woocommerce-subscriptions' ), $item, $_product, $subscription ); ?>
<a href="<?php echo esc_url( WCS_Remove_Item::get_remove_url( $subscription->get_id(), $item_id ) ); ?>" class="remove" onclick="return confirm('<?php printf( esc_html( $confirm_notice ) ); ?>');">&times;</a>
<?php
// Translators: %s: product name.
$aria_label = sprintf( __( 'Remove %s', 'woocommerce-subscriptions' ), esc_html( $_product->get_name() ) );
$confirm_notice = apply_filters( 'woocommerce_subscriptions_order_item_remove_confirmation_text', __( 'Are you sure you want to remove this item from your subscription?', 'woocommerce-subscriptions' ), $item, $_product, $subscription );
?>
<a
href="<?php echo esc_url( WCS_Remove_Item::get_remove_url( $subscription->get_id(), $item_id ) ); ?>"
class="remove"
role="button"
onclick="return confirm('<?php printf( esc_html( $confirm_notice ) ); ?>');"
aria-haspopup="dialog"
aria-label="<?php echo esc_attr( $aria_label ); ?>"
>
&times;
</a>
<?php endif; ?>
</td>
<?php endif; ?>

View File

@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => 'woocommerce/woocommerce-subscriptions',
'pretty_version' => 'dev-release/8.1.0',
'version' => 'dev-release/8.1.0',
'reference' => '073151600fa05dc26b7244faf4808c0f49e63436',
'pretty_version' => 'dev-release/8.2.0',
'version' => 'dev-release/8.2.0',
'reference' => '649a58503cd3715ecc5d37281a2cbb62fe5ad320',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -29,9 +29,9 @@
'dev_requirement' => false,
),
'woocommerce/woocommerce-subscriptions' => array(
'pretty_version' => 'dev-release/8.1.0',
'version' => 'dev-release/8.1.0',
'reference' => '073151600fa05dc26b7244faf4808c0f49e63436',
'pretty_version' => 'dev-release/8.2.0',
'version' => 'dev-release/8.2.0',
'reference' => '649a58503cd3715ecc5d37281a2cbb62fe5ad320',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),

View File

@ -5,11 +5,11 @@
* Description: Sell products and services with recurring payments in your WooCommerce Store.
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* Version: 8.1.0
* Version: 8.2.0
* Requires Plugins: woocommerce
*
* WC requires at least: 10.2.0
* WC tested up to: 10.3.0
* WC requires at least: 10.3.0
* WC tested up to: 10.4.0
* Requires PHP: 7.4
*
* License: GNU General Public License v3.0
@ -84,7 +84,7 @@ class WC_Subscriptions {
public static $plugin_file = __FILE__;
/** @var string */
public static $version = '8.1.0'; // WRCS: DEFINED_VERSION.
public static $version = '8.2.0'; // WRCS: DEFINED_VERSION.
/** @var string */
public static $wc_minimum_supported_version = '7.7';