Compare commits

...

3 Commits
v8.1.0 ... main

Author SHA1 Message Date
WooCommerce 56e03c0544 Updates to 8.3.0 2026-01-08 10:21:29 +00:00
WooCommerce bfe7fb609d Updates to 8.2.1 2025-12-24 10:19:37 +00:00
WooCommerce c7e30d354e Updates to 8.2.0 2025-12-11 10:21:12 +00:00
70 changed files with 10654 additions and 432 deletions

View File

@ -77,6 +77,14 @@ a.close-subscriptions-search {
display: block;
width: 50%;
}
.woocommerce_options_panel ._subscription_downloads_field .description,
.woocommerce_options_panel .subscription_linked_downloadable_products .description {
clear: both;
display: block;
margin: 0;
}
/* Variation Subscription Product Sync Settings */
.variable_subscription_sync .subscription_sync_annual > .select2-container {
max-width: 48%;
@ -385,6 +393,12 @@ p._subscription_gifting_field_description.form-field .description {
opacity: 0.5;
}
#woocommerce-product-data
.form-field._subscription_downloads_field
.select2-selection__choice {
font-size: 14px;
}
/* Variation Pricing Fields with WooCommerce 3.0+ */
.wc-metaboxes-wrapper .variable_subscription_trial label,
.wc-metaboxes-wrapper .variable_subscription_pricing label,

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

@ -639,6 +639,53 @@ jQuery( function ( $ ) {
},
} );
/**
* Relocates the linked downloadable product fields in the context of a variable subscription product, and
* selectively shows/hides it, depending on whether the variation is downloadable or not.
*/
function initLinkedDownloadableProductFieldsForVariationProduct() {
$( '#variable_product_options .variable_subscription_linked_downloadable_products' )
.not( '.wcs_moved' )
.each( function () {
const $this = $( this );
const $variablePricing = $( this ).siblings( '.variable_pricing' );
const $downloadableFiles = $( this ).siblings( '.form-row.downloadable_files' ).parent( 'div' );
$this.insertAfter( $downloadableFiles );
$this.addClass( 'wcs_gifting_moved' );
} );
}
/**
* Move the linked downloadable product fields to the correct location (which is underneath the downloadable files
* section).
*/
function relocateLinkedDownloadableProductFields() {
const $linkedDownloadableProducts = $( '.subscription_linked_downloadable_products_section' );
const $productTypeSelector = $('select#product-type');
const showHide = () => {
$productTypeSelector.val() === WCSubscriptions.productType
? $linkedDownloadableProducts.show()
: $linkedDownloadableProducts.hide();
};
$linkedDownloadableProducts
.not( '.wcs_moved' )
.each( function () {
const $downloadableFilesSection = $( '.form-field.downloadable_files' ).parent( 'div' );
if ( $downloadableFilesSection.length === 1 ) {
$( this ).insertAfter( $downloadableFilesSection );
$( this ).addClass( 'wcs_moved' );
}
} );
showHide();
$productTypeSelector.on( 'change', showHide );
}
$( '.options_group.pricing ._sale_price_field .description' ).prepend(
'<span id="sale-price-period" style="display: none;"></span>'
);
@ -653,9 +700,9 @@ jQuery( function ( $ ) {
$( '.options_group.subscription_pricing' )
);
// Move the subscription variation pricing and gifting sections to a better location in the DOM on load
// We do this because these sections are initially loaded at the end of the variable product options section,
// which is not the best location for them, so we move to before the "Sale price" section.
// Move the subscription variation pricing, gifting and linked downloadable product sections to a better location in
// the DOM on load. We do this because these sections are initially loaded at the end of the variable product
// options section, which is not the best location for them, so we move to before the "Sale price" section.
if (
$( '#variable_product_options .variable_subscription_pricing' ).length >
0
@ -668,6 +715,7 @@ jQuery( function ( $ ) {
) {
$.moveSubscriptionGiftingFields();
}
// When a variation is added
$( '#woocommerce-product-data' ).on(
'woocommerce_variations_added woocommerce_variations_loaded',
@ -677,6 +725,7 @@ jQuery( function ( $ ) {
$.showHideVariableSubscriptionMeta();
$.showHideSyncOptions();
$.setSubscriptionLengths();
initLinkedDownloadableProductFieldsForVariationProduct();
}
);
@ -690,6 +739,7 @@ jQuery( function ( $ ) {
$.showHideSyncOptions();
$.disableEnableOneTimeShipping();
$.showHideSubscriptionsPanels();
relocateLinkedDownloadableProductFields();
}
// Update subscription ranges when subscription period or interval is changed

View File

@ -1,31 +0,0 @@
/* global wc_subscription_downloads_product */
jQuery( document ).ready( function ( $ ) {
function subscriptionDownloadsChosen() {
$( 'select.subscription-downloads-ids' ).ajaxChosen({
method: 'GET',
url: wc_subscription_downloads_product.ajax_url,
dataType: 'json',
afterTypeDelay: 100,
minTermLength: 1,
data: {
action: 'wc_subscription_downloads_search',
security: wc_subscription_downloads_product.security
}
}, function ( data ) {
var orders = {};
$.each( data, function ( i, val ) {
orders[i] = val;
});
return orders;
});
}
subscriptionDownloadsChosen();
$( 'body' ).on( 'woocommerce_variations_added', function () {
subscriptionDownloadsChosen();
});
});

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

@ -18,7 +18,8 @@ jQuery( document ).ready( function ( $ ) {
.find( '.recipient_email' )
.trigger( 'focus' );
}
} );
} )
.removeClass( 'hidden' );
const shipToDifferentAddressCheckbox = $( document ).find(
'#ship-to-different-address-checkbox'
@ -31,12 +32,14 @@ jQuery( document ).ready( function ( $ ) {
$( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.wcsg_add_recipient_fields' )
.slideUp( 250 );
.slideUp( 250 )
.addClass( 'hidden' );
const recipientEmailElement = $( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.recipient_email' );
recipientEmailElement.val( '' );
hideValidationErrorForEmailField( recipientEmailElement );
setShippingAddressNoticeVisibility( true );
if ( $( 'form.checkout' ).length !== 0 ) {

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();
}
}
} );

1
build/admin.asset.php Normal file
View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-i18n', 'wp-primitives'), 'version' => '8bad0ce18409676b15d1');

1
build/admin.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'b52cf873a54e347c8f9e');

File diff suppressed because one or more lines are too long

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' => '32bfa486d902830f29c7');

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wc-blocks-checkout', 'wp-components', 'wp-data', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'e7869a0a766c0f65ff8a');
<?php return array('dependencies' => array('react', 'react-dom', 'wc-blocks-checkout', 'wp-components', 'wp-data', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'cf4dd71c0c7418b591fd');

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,43 @@
*** WooCommerce Subscriptions Changelog ***
2026-01-06 - version 8.3.0
* Add: Support for ajax add-to-cart flows from the single product page, when upgrading or downgrading subscriptions.
* Add: Enhanced functionality from the WooCommerce Subscription Downloads plugin has now been integrated directly into WooCommerce Subscriptions.
* Fix: Ensured the subject line for the "Customer Renewal Invoice" email correctly updates.
2025-12-22 - version 8.2.1
* Fix: Subscription pricing strings now correctly display singular and plural billing intervals like "$10 / month" and "$10 every 3 months" in cart and checkout totals.
* Fix: Display all recurring coupon names in the totals section of classic cart and checkout when multiple recurring coupons are applied.
* Fix: Payment method selector options on Subscriptions list page are now properly displaying the payment method name for gateways providing multiple payment methods.
* Fix: PayPal Standard plugin IPN renewal orders not being created for failed payment retries.
* Fix: Prevent fatal errors on shop pages when special characters like % are used in a product name.
* Fix: Prevent an email validation error from briefly and inadvertently flashing on the screen when the "this is a gift" checkbox is used.
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

@ -0,0 +1,59 @@
<?php
/**
* WCS_Admin_Assets Class
*
* Handles admin assets (scripts and styles) for WooCommerce Subscriptions.
*
* @package WooCommerce Subscriptions/Admin
*/
class WCS_Admin_Assets {
/**
* Initialize the tour handler
*/
public static function init() {
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
}
/**
* Enqueue required scripts and styles
*/
public static function enqueue_scripts() {
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/admin.asset.php' );
$script_asset = file_exists( $script_asset_path )
? require $script_asset_path
: array(
'dependencies' => array(
'react',
'wc-blocks-checkout',
'wc-price-format',
'wc-settings',
'wp-element',
'wp-i18n',
'wp-plugins',
),
'version' => WC_Subscriptions::$version,
);
wp_enqueue_script(
'wcs-admin',
plugins_url( '/build/admin.js', WC_Subscriptions::$plugin_file ),
$script_asset['dependencies'],
$script_asset['version'],
true
);
wp_enqueue_style(
'wcs-admin',
plugins_url( '/build/style-admin.css', WC_Subscriptions::$plugin_file ),
array( 'wp-components' ),
$script_asset['version']
);
wp_set_script_translations(
'wcs-admin',
'woocommerce-subscriptions',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'languages'
);
}
}

View File

@ -24,6 +24,7 @@ class WC_REST_Subscriptions_Settings_Option_Controller extends WP_REST_Controlle
*/
private const ALLOWED_OPTIONS = [
'woocommerce_subscriptions_gifting_is_welcome_announcement_dismissed',
'woocommerce_subscriptions_downloads_is_welcome_announcement_dismissed',
];
/**

View File

@ -34,6 +34,7 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
WCS_Call_To_Action_Button_Text_Manager::init();
WCS_Subscriber_Role_Manager::init();
WCS_Upgrade_Notice_Manager::init();
WCS_Admin_Assets::init();
$tracks_events = new WC_Tracks_Events();
$tracks_events->setup();
@ -45,6 +46,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' ) );
}
/**
@ -299,10 +301,16 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
*/
public function init_downloads() {
if (
! defined( 'WCS_ALLOW_SUBSCRIPTION_DOWNLOADS' )
|| $this->is_plugin_being_activated( 'woocommerce-subscription-downloads' )
$this->is_plugin_being_activated( 'woocommerce-subscription-downloads' )
|| class_exists( WC_Subscription_Downloads::class, false )
) {
if ( class_exists( WC_Subscription_Downloads::class, false ) ) {
// Will show the welcome announcement if the standalone plugin is active and the welcome announcement has not been dismissed.
if ( ! WC_Subscription_Downloads_Admin_Welcome_Announcement::is_welcome_announcement_dismissed() ) {
WC_Subscription_Downloads_Admin_Welcome_Announcement::init();
}
}
return;
}

View File

@ -45,6 +45,7 @@ class WCS_Autoloader extends WCS_Core_Autoloader {
'wcs_webhooks' => true,
'wcs_auth' => true,
'wcs_upgrade_notice_manager' => true,
'wcs_admin_assets' => true,
'wc_subscriptions_cli' => true,
);

View File

@ -1171,7 +1171,7 @@ class WCS_Admin_Post_Types {
// @phpstan-ignore property.notFound
foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) {
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id === $gateway_id ? 'selected' : '' ) . '>' . esc_html( $gateway->title ) . '</option>';
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id === $gateway_id ? 'selected' : '' ) . '>' . esc_html( method_exists( $gateway, 'get_title' ) ? $gateway->get_title() : $gateway->title ) . '</option>';
}
echo '<option value="_manual_renewal">' . esc_html__( 'Manual Renewal', 'woocommerce-subscriptions' ) . '</option>';
?>

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', $text, $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

@ -57,6 +57,43 @@ class WCS_Limiter {
do_action( 'woocommerce_subscriptions_product_options_advanced' );
}
/**
* Checks if the session contains a renewal for a given product.
* Used for the pay for order flow.
*
* @param WC_Product $product The product to check.
* @return bool
*/
private static function session_contains_renewal( $product ) {
if ( ! empty( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( (int) $product->get_id() === (int) $cart_item['product_id'] && isset( $cart_item['subscription_renewal'] ) ) {
return true;
}
}
}
return false;
}
/**
* Checks if the session contains a resubscribe for a given product.
* Used for the pay for order flow with limited subscriptions products.
*
* @param WC_Product $product The product to check.
* @return bool
*/
private static function session_contains_resubscribe( $product ) {
if ( ! empty( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( (int) $product->get_id() === (int) $cart_item['product_id'] && isset( $cart_item['subscription_resubscribe'] ) ) {
return true;
}
}
}
return false;
}
/**
* Canonical is_purchasable method to be called by product classes.
*
@ -68,73 +105,62 @@ class WCS_Limiter {
*/
public static function is_purchasable( $purchasable, $product ) {
// Prevents making a non purchasable product purchasable again.
// This can happen if the product is disabled and limited and the customer is trying to renew the subscription for example.
if ( ! $purchasable ) {
return $purchasable;
// Check if product is private (for variations, also check parent product)
$is_private_product = 'private' === $product->get_status();
if ( $product->get_parent_id() > 0 ) {
$parent_product = wc_get_product( $product->get_parent_id() );
if ( $parent_product ) {
$is_private_product = 'private' === $parent_product->get_status();
}
}
switch ( $product->get_type() ) {
case 'subscription':
case 'variable-subscription':
// Checks if the product is limited.
if ( false === self::is_product_limited( $purchasable, $product ) ) {
// Product is limited, so it is not purchasable.
// Checks limits for variable subscription products.
if ( $product->get_type() === 'subscription_variation' && isset( $parent_product ) ) {
if ( $is_private_product ) {
$purchasable = false;
// Forces product to be available when processing a renewal order. Allowing people to renew private products.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
if ( self::is_paying_for_failed_renewal_order( $parent_product ) || isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() || self::session_contains_renewal( $parent_product ) ) {
$purchasable = true;
}
}
if ( 'no' !== wcs_get_product_limitation( $parent_product ) && ( ! empty( WC()->cart->cart_contents ) || self::session_contains_resubscribe( $parent_product ) ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
// When mixed checkout is disabled, the variation is replaceable.
if ( 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) {
foreach ( WC()->cart->cart_contents as $cart_item ) {
// If the variable product is limited, it can't be purchased if it is the same variation
if ( $product->get_parent_id() === $cart_item['data']->get_parent_id() && $product->get_id() !== $cart_item['data']->get_id() ) {
$purchasable = false;
break;
}
}
}
}
} else { // Checks limits for simple subscription products.
if ( $is_private_product ) {
$purchasable = false;
// Forces product to be available when processing a renewal order. Allowing people to renew private products.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
if ( self::is_paying_for_failed_renewal_order( $product ) || isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() || self::session_contains_renewal( $product ) ) {
$purchasable = true;
}
}
// This actually means the product is limited when returning false.
if ( false === self::is_product_limited( $purchasable, $product ) ) {
$resubscribe_cart_item = wcs_cart_contains_resubscribe();
// Allows the product to be resubscribed but not purchased again.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
if ( empty( $_GET['resubscribe'] ) && false === $resubscribe_cart_item && false === self::session_contains_resubscribe( $product ) ) {
$purchasable = false;
// Unless it's resubscribing, renewing or restoring cart from session.
$resubscribe_cart_item = wcs_cart_contains_resubscribe();
// Resubscribe logic
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['resubscribe'] ) || false !== $resubscribe_cart_item ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$subscription_id = ( isset( $_GET['resubscribe'] ) ) ? absint( $_GET['resubscribe'] ) : $resubscribe_cart_item['subscription_resubscribe']['subscription_id'];
$subscription = wcs_get_subscription( $subscription_id );
if ( $subscription && $subscription->has_product( $product->get_id() ) && wcs_can_user_resubscribe_to( $subscription ) ) {
$purchasable = true;
}
// Renewal logic
} elseif (
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
isset( $_GET['subscription_renewal'] ) ||
wcs_cart_contains_renewal()
) {
$purchasable = true;
// Restoring cart from session, so need to check the cart in the session (wcs_cart_contains_renewal() only checks the cart).
} elseif ( ! empty( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( (int) $product->get_id() === (int) $cart_item['product_id'] && ( isset( $cart_item['subscription_renewal'] ) || isset( $cart_item['subscription_resubscribe'] ) ) ) {
$purchasable = true;
break;
}
}
}
}
break;
case 'subscription_variation':
$variable_product = wc_get_product( $product->get_parent_id() );
if ( 'no' != wcs_get_product_limitation( $variable_product ) && ! empty( WC()->cart->cart_contents ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
// When mixed checkout is disabled, the variation is replaceable
if ( 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) {
$purchasable = true;
} else { // When mixed checkout is enabled
foreach ( WC()->cart->cart_contents as $cart_item ) {
// If the variable product is limited, it can't be purchased if it is the same variation
if ( $product->get_parent_id() === $cart_item['data']->get_parent_id() && $product->get_id() !== $cart_item['data']->get_id() ) {
$purchasable = false;
break;
}
}
}
}
break;
}
}
return $purchasable;
}
@ -302,12 +328,73 @@ class WCS_Limiter {
return self::$order_awaiting_payment_for_product[ $product_id ];
}
/**
* Check if we're currently paying for a failed renewal order containing the product.
*
* @since 8.3.0 - Migrated from WooCommerce Subscriptions v2.1.0
* @param WC_Product $product The product to check.
* @return bool
*/
protected static function is_paying_for_failed_renewal_order( $product ) {
global $wp;
// Check if we're on the pay for order page
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['pay_for_order'] ) || ! isset( $_GET['key'] ) || ! isset( $wp->query_vars['order-pay'] ) ) {
// Also check if cart contains a failed renewal order payment
$failed_renewal_cart_item = wcs_cart_contains_failed_renewal_order_payment();
if ( false !== $failed_renewal_cart_item ) {
$cart_item_product_id = isset( $failed_renewal_cart_item['variation_id'] ) && $failed_renewal_cart_item['variation_id'] > 0
? $failed_renewal_cart_item['variation_id']
: $failed_renewal_cart_item['product_id'];
// Check both the product ID and parent product ID (for variations)
if ( (int) $product->get_id() === (int) $cart_item_product_id || (int) $product->get_id() === (int) $failed_renewal_cart_item['product_id'] ) {
return true;
}
// Also check if product is a variation and matches the parent
if ( $product->get_parent_id() > 0 && (int) $product->get_parent_id() === (int) $failed_renewal_cart_item['product_id'] ) {
return true;
}
}
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : '';
$order_id = isset( $wp->query_vars['order-pay'] ) ? $wp->query_vars['order-pay'] : 0;
$order = wc_get_order( absint( $order_id ) );
if ( ! $order || $order->get_order_key() !== $order_key ) {
return false;
}
// Check if order is a failed renewal order
if ( ! $order->has_status( 'failed' ) && ! wcs_order_contains_renewal( $order ) ) {
return false;
}
// Check if the order contains the product
foreach ( $order->get_items() as $item ) {
$item_product_id = isset( $item['variation_id'] ) && $item['variation_id'] > 0 ? $item['variation_id'] : $item['product_id'];
// Check both the product ID and parent product ID (for variations)
if ( (int) $product->get_id() === (int) $item_product_id || (int) $product->get_id() === (int) $item['product_id'] ) {
return true;
}
// Also check if product is a variation and matches the parent
if ( $product->get_parent_id() > 0 && (int) $product->get_parent_id() === (int) $item['product_id'] ) {
return true;
}
}
return false;
}
/**
* Filters the order statuses that enable the order again button and functionality.
*
* This function will return no statuses if the order contains non purchasable or limited products.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v3.0.2
* @since 8.3.0 - Migrated from WooCommerce Subscriptions v3.0.2
*
* @param array $statuses The order statuses that enable the order again button.
* @return array $statuses An empty array if the order contains limited products, otherwise the default statuses are returned.

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' );
}
/**
@ -127,7 +124,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
* @return string
*/
function get_subject() {
return apply_filters( 'woocommerce_subscriptions_email_subject_new_renewal_order', parent::get_subject(), $this->object );
return apply_filters( 'woocommerce_subscriptions_email_subject_new_renewal_order', $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object );
}
/**
@ -137,7 +134,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
* @return string
*/
function get_heading() {
return apply_filters( 'woocommerce_email_heading_customer_renewal_order', parent::get_heading(), $this->object );
return apply_filters( 'woocommerce_email_heading_customer_renewal_order', $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object );
}
/**

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,7 +190,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
$transaction_order = wc_get_order( substr( $transaction_details['invoice'], strrpos( $transaction_details['invoice'], '-' ) + 1 ) );
// check if the failed signup has been previously recorded
if ( wcs_get_objects_property( $transaction_order, 'id' ) !== $subscription->get_meta( '_paypal_failed_sign_up_recorded', true ) ) {
if ( wcs_get_objects_property( $transaction_order, 'id' ) !== (int) $subscription->get_meta( '_paypal_failed_sign_up_recorded', true ) ) {
$is_renewal_sign_up_after_failure = true;
}
}

View File

@ -227,9 +227,8 @@ class WC_Subscriptions_Upgrader {
WCS_Plugin_Upgrade_7_8_0::check_gifting_plugin_is_enabled();
}
if ( false && version_compare( self::$stored_plugin_version, '8.2.0', '<' ) ) {
// TODO: remove false from the above conditional, once we are ready to make subscription downloads functionality available.
WCS_Plugin_Upgrade_8_1_0::check_downloads_plugin_is_enabled();
if ( version_compare( self::$stored_plugin_version, '8.3.0', '<' ) ) {
WCS_Plugin_Upgrade_8_3_0::check_downloads_plugin_is_enabled();
}
}

View File

@ -7,7 +7,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Plugin_Upgrade_8_1_0 {
class WCS_Plugin_Upgrade_8_3_0 {
/**
* Check if the Gifting plugin is enabled and update the settings.

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

@ -0,0 +1,93 @@
<?php
/**
* Downloads Admin Announcement Handler Class
*
* @package WooCommerce Subscriptions
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_Subscription_Downloads_Admin_Welcome_Announcement {
/**
* Initialize the tour handler
*/
public static function init() {
// Register scripts and styles
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
// Add the tour HTML to the admin footer
add_action( 'admin_footer', array( __CLASS__, 'output_tour' ) );
}
/**
* Enqueue required scripts and styles
*/
public static function enqueue_scripts() {
if ( ! self::is_woocommerce_admin_or_subscriptions_listing() ) {
return;
}
$screen = get_current_screen();
wp_localize_script(
'wcs-admin',
'wcsDownloadsSettings',
array(
'imagesPath' => plugins_url( '/assets/images', WC_Subscriptions::$plugin_file ),
'pluginsUrl' => admin_url( 'plugins.php' ),
'subscriptionsUrl' => WC_Subscriptions_Admin::settings_tab_url() . '#woocommerce_subscriptions_downloads_enable',
'isStandaloneDownloadsEnabled' => is_plugin_active( 'woocommerce-subscription-downloads/woocommerce-subscription-downloads.php' ),
'isSubscriptionsListing' => 'woocommerce_page_wc-orders--shop_subscription' === $screen->id,
)
);
}
/**
* Output the tour HTML in the admin footer
*/
public static function output_tour() {
if ( ! self::is_woocommerce_admin_or_subscriptions_listing() ) {
return;
}
// Add a div for the tour to be rendered into
echo '<div id="wcs-downloads-welcome-announcement-root" class="woocommerce-tour-kit"></div>';
}
/**
* Checks if the welcome tour has been dismissed.
*
* @return bool
*/
public static function is_welcome_announcement_dismissed() {
return '1' === get_option(
'woocommerce_subscriptions_downloads_is_welcome_announcement_dismissed',
''
);
}
/**
* Checks if the current screen is WooCommerce Admin or subscriptions listing.
*
* @return bool
*/
private static function is_woocommerce_admin_or_subscriptions_listing() {
$screen = get_current_screen();
if ( ! $screen ) {
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action_param = isset( $_GET['action'] ) ? wc_clean( wp_unslash( $_GET['action'] ) ) : '';
$is_woocommerce_admin = 'woocommerce_page_wc-admin' === $screen->id;
$is_subscriptions_listing = 'woocommerce_page_wc-orders--shop_subscription' === $screen->id && empty( $action_param );
return $is_woocommerce_admin || $is_subscriptions_listing;
}
}

View File

@ -17,6 +17,7 @@ class WC_Subscription_Downloads_Ajax {
*/
public function __construct() {
add_action( 'wp_ajax_wc_subscription_downloads_search', array( $this, 'search_subscriptions' ) );
add_action( 'wp_ajax_wc_subscription_linked_downloadable_products_search', array( $this, 'search_downloadable_products' ) );
}
/**
@ -72,4 +73,39 @@ class WC_Subscription_Downloads_Ajax {
wp_send_json( $found_subscriptions );
}
/**
* Searches for downloadable products that are simple or variants.
*
* @return void
*/
public function search_downloadable_products(): void {
$results = array();
// Prevent error noise from leaking.
ob_start();
if ( isset( $_GET['term'] ) && check_ajax_referer( 'search-products', 'security' ) ) {
$term = wc_clean( wp_unslash( $_GET['term'] ) );
}
if ( ! empty( $term ) ) {
$products = wc_get_products(
array(
'downloadable' => true,
'limit' => 100,
's' => $term,
'type' => array( 'simple', 'variation' ),
'status' => 'any',
)
);
foreach ( $products as $product ) {
$results[ $product->get_id() ] = sanitize_text_field( $product->get_formatted_name() );
}
}
ob_clean();
wp_send_json( $results );
}
}

View File

@ -7,52 +7,202 @@ if ( ! defined( 'ABSPATH' ) ) {
* WooCommerce Subscription Downloads Products.
*
* @package WC_Subscription_Downloads_Products
* @category Products
* @author WooThemes
*/
class WC_Subscription_Downloads_Products {
public const EDITOR_UPDATE = 'wcsubs_subscription_download_relationships';
public const RELATIONSHIP_DOWNLOAD_TO_SUB = 'download-to-sub';
public const RELATIONSHIP_VAR_DOWNLOAD_TO_SUB = 'var-download-to-sub';
public const RELATIONSHIP_SUB_TO_DOWNLOAD = 'sub-to-download';
public const RELATIONSHIP_VAR_SUB_TO_DOWNLOAD = 'var-sub-to-download';
/**
* Products actions.
*/
public function __construct() {
add_action( 'woocommerce_product_options_downloads', array( $this, 'simple_write_panel_options' ), 10 );
add_action( 'woocommerce_product_options_downloads', array( $this, 'simple_write_panel_options' ) );
add_action( 'woocommerce_variation_options_download', array( $this, 'variable_write_panel_options' ), 10, 3 );
add_action( 'admin_enqueue_scripts', array( $this, 'scripts' ) );
add_action( 'woocommerce_process_product_meta_simple', array( $this, 'save_simple_product_data' ), 10 );
add_action( 'woocommerce_save_product_variation', array( $this, 'save_variation_product_data' ), 10, 2 );
add_action( 'woocommerce_product_options_pricing', array( $this, 'subscription_product_editor_ui' ) );
add_action( 'woocommerce_variable_subscription_pricing', array( $this, 'variable_subscription_product_editor_ui' ), 10, 3 );
add_action( 'save_post_product', array( $this, 'handle_product_save' ) );
add_action( 'save_post_product_variation', array( $this, 'handle_product_variation_save' ) );
add_action( 'woocommerce_save_product_variation', array( $this, 'handle_product_variation_save' ) );
add_action( 'woocommerce_update_product', array( $this, 'handle_product_save' ) );
add_action( 'woocommerce_update_product_variation', array( $this, 'handle_product_variation_save' ) );
add_action( 'init', array( $this, 'init' ) );
}
public function init() {
if ( version_compare( WC_VERSION, '3.0', '<' ) ) {
add_action( 'woocommerce_duplicate_product', array( $this, 'save_subscriptions_when_duplicating_product' ), 10, 2 );
} else {
add_action( 'woocommerce_product_duplicate', array( $this, 'save_subscriptions_when_duplicating_product' ), 10, 2 );
add_action( 'woocommerce_product_duplicate', array( $this, 'save_subscriptions_when_duplicating_product' ), 10, 2 );
}
/**
* Handle product save - generic handler for all product updates.
*
* @param int $post_id Post ID.
*
* @return void
*/
public function handle_product_save( $post_id ) {
// Bail if this is an autosave or revision.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
$product = wc_get_product( $post_id );
if ( ! $product ) {
return;
}
$product_is_downloadable = $product->is_downloadable();
$product_is_subscription = $product->is_type( array( 'subscription', 'variable-subscription' ) );
// We do not allow downloadable subscription products to be linked with other subscription products; this is
// principally to avoid confusion (though it would be technically feasible).
if ( $product_is_subscription && ! $product_is_downloadable ) {
$this->handle_subscription_product_save( $post_id );
} elseif ( ! $product_is_subscription && $product_is_downloadable ) {
$this->handle_downloadable_product_save( $post_id );
}
}
/**
* Product screen scripts.
* Handle product variation save - generic handler for all variation updates.
*
* @param int $post_id Post ID.
*
* @return void
*/
public function scripts() {
$screen = get_current_screen();
public function handle_product_variation_save( $post_id ) {
// Bail if this is an autosave or revision.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( 'product' == $screen->id && version_compare( WC_VERSION, '2.3.0', '<' ) ) {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
if ( wp_is_post_revision( $post_id ) ) {
return;
}
wp_enqueue_script( 'wc_subscription_downloads_writepanel', plugins_url( 'assets/js/admin/writepanel' . $suffix . '.js', plugin_dir_path( __FILE__ ) ), array( 'ajax-chosen', 'chosen' ), WC_Subscriptions::$version, true );
$variation = wc_get_product( $post_id );
if ( ! $variation || ! $variation->is_type( 'variation' ) ) {
return;
}
wp_localize_script(
'wc_subscription_downloads_writepanel',
'wc_subscription_downloads_product',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'security' => wp_create_nonce( 'search-products' ),
)
);
// Handle downloadable variations (they link TO subscriptions).
if ( $variation->is_downloadable() ) {
$this->handle_downloadable_product_save( $post_id );
}
// Handle subscription variations (they link TO downloadable products).
$parent = wc_get_product( $variation->get_parent_id() );
if ( $parent && $parent->is_type( 'variable-subscription' ) ) {
$this->handle_subscription_product_save( $post_id );
}
}
/**
* Handle save for downloadable products (simple or variation).
* These products link TO subscription products.
*
* @param int $product_id Product or variation ID.
*
* @return void
*/
private function handle_downloadable_product_save( $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return;
}
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if (
isset( $_POST[ self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $product_id ] )
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $product_id ], self::EDITOR_UPDATE )
) {
$subscription_ids = wc_clean( wp_unslash( $_POST['_variable_subscription_downloads_ids'][ $product_id ] ?? array() ) );
$subscription_ids = array_filter( (array) $subscription_ids );
$this->update_subscription_downloads( $product_id, $subscription_ids );
}
if (
isset( $_POST[ self::RELATIONSHIP_DOWNLOAD_TO_SUB ] )
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_DOWNLOAD_TO_SUB ], self::EDITOR_UPDATE )
) {
$subscription_ids = wc_clean( wp_unslash( $_POST['_subscription_downloads_ids'] ?? array() ) );
$subscription_ids = array_filter( (array) $subscription_ids );
$this->update_subscription_downloads( $product_id, $subscription_ids );
}
// Observe and act on product status changes (regardless of whether they were made from within the product
// editor, therefore we don't care about nonce checks here).
$this->assess_downloadable_product_status( $product_id );
// phpcs:enable
}
/**
* Handle save for subscription products (simple subscription or variation).
* These products link TO downloadable products.
*
* @param int $product_id Subscription product or variation ID.
*
* @return void
*/
private function handle_subscription_product_save( $product_id ) {
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if (
isset( $_POST[ self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $product_id ] )
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $product_id ], self::EDITOR_UPDATE )
) {
$product_ids = wc_clean( wp_unslash( $_POST[ '_subscription_linked_downloadable_products_' . $product_id ] ?? array() ) );
$product_ids = array_filter( (array) $product_ids );
$this->update_subscription_products( $product_id, $product_ids );
return;
}
if (
isset( $_POST[ self::RELATIONSHIP_SUB_TO_DOWNLOAD ] )
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_SUB_TO_DOWNLOAD ], self::EDITOR_UPDATE )
) {
$product_ids = wc_clean( wp_unslash( (array) $_POST['_subscription_linked_downloadable_products'] ?? array() ) );
$product_ids = array_filter( (array) $product_ids );
$this->update_subscription_products( $product_id, $product_ids );
}
// phpcs:enable
}
/**
* Assess downloadable product status and adjust permissions accordingly.
* Called when no form data is available (e.g., status change, REST API update, file changes).
*
* @param int $product_id Product ID.
*
* @return void
*/
private function assess_downloadable_product_status( $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product || ! $product->is_downloadable() ) {
return;
}
$status_object = get_post_status_object( $product->get_status() );
$is_public = $status_object && $status_object->public;
// Always revoke existing permissions first to ensure clean state.
// This handles file changes and status transitions.
$this->revoke_permissions_for_product( $product_id );
// Grant fresh permissions only if product is public.
if ( $is_public ) {
$this->grant_permissions_for_product( $product_id );
}
}
@ -62,28 +212,27 @@ class WC_Subscription_Downloads_Products {
public function simple_write_panel_options() {
global $post;
?>
<p class="form-field _subscription_downloads_field">
<label for="subscription-downloads-ids"><?php esc_html_e( 'Included subscription products', 'woocommerce-subscriptions' ); ?></label>
<p class="form-field _subscription_downloads_field hide_if_subscription">
<label for="subscription-downloads-ids"><?php esc_html_e( 'Linked subscription products', 'woocommerce-subscriptions' ); ?></label>
<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 );
$subscription = wc_get_product( $subscription_id );
if ( $_subscription ) {
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $_subscription->get_formatted_name() ) ) . '</option>';
if ( $subscription ) {
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $subscription->get_formatted_name() ) ) . '</option>';
}
}
}
?>
</select>
<?php echo wc_help_tip( wc_sanitize_tooltip( __( 'Select subscription products that will include this downloadable product.', 'woocommerce-subscriptions' ) ) ); ?>
<span class="description"><?php esc_html_e( 'Select subscription products that will include this downloadable product.', 'woocommerce-subscriptions' ); ?></span>
<?php wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_DOWNLOAD_TO_SUB, false ); ?>
</p>
<?php
@ -96,30 +245,145 @@ class WC_Subscription_Downloads_Products {
?>
<tr class="show_if_variation_downloadable">
<td colspan="2">
<p class="form-field form-row form-row-full">
<label><?php esc_html_e( 'Included subscription products', 'woocommerce-subscriptions' ); ?>:</label>
<p class="form-field _subscription_downloads_field form-row form-row-full hide_if_variable-subscription">
<label><?php esc_html_e( 'Linked subscription products', 'woocommerce-subscriptions' ); ?>:</label>
<?php echo wc_help_tip( wc_sanitize_tooltip( __( 'Select subscription products that will include this downloadable product.', 'woocommerce-subscriptions' ) ) ); ?>
<select multiple="multiple" data-placeholder="<?php esc_html_e( 'Select subscriptions', 'woocommerce-subscriptions' ); ?>" class="subscription-downloads-ids wc-product-search" name="_variable_subscription_downloads_ids[<?php echo esc_attr( $loop ); ?>][]" style="width: 100%">
<select multiple="multiple" data-placeholder="<?php esc_html_e( 'Select subscriptions', 'woocommerce-subscriptions' ); ?>" class="subscription-downloads-ids wc-product-search" name="_variable_subscription_downloads_ids[<?php echo esc_attr( $variation->ID ); ?>][]" style="width: 100%">
<?php
$subscriptions_ids = WC_Subscription_Downloads::get_subscriptions( $variation->ID );
if ( $subscriptions_ids ) {
foreach ( $subscriptions_ids as $subscription_id ) {
$_subscription = wc_get_product( $subscription_id );
$subscription = wc_get_product( $subscription_id );
if ( $_subscription ) {
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $_subscription->get_formatted_name() ) ) . '</option>';
if ( $subscription ) {
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $subscription->get_formatted_name() ) ) . '</option>';
}
}
}
?>
</select>
<?php wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $variation->ID, false ); ?>
</p>
</td>
</tr>
<?php
}
/**
* Adds a field with which to link the subscription product (the product being edited) with zero-or-many
* downloadable products.
*
* @return void
*/
public function subscription_product_editor_ui(): void {
global $post;
if ( ! $post instanceof WP_Post ) {
wc_get_logger()->warning(
'Unable to add the downloadable products selector to the product editor (global post object is unavailable).',
array( 'backtrace' => true )
);
return;
}
$description = esc_html__( 'Select simple and variable downloadable products that will be included with this subscription product.', 'woocommerce-subscriptions' );
$label = esc_html__( 'Linked downloadable products', 'woocommerce-subscriptions' );
$linked_products = '';
$nonce_field = wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_SUB_TO_DOWNLOAD, false, false );
$placeholder = esc_attr__( 'Select products', 'woocommerce-subscriptions' );
foreach ( WC_Subscription_Downloads::get_downloadable_products( $post->ID ) as $product_id ) {
$product_id = absint( $product_id );
$product = wc_get_product( $product_id );
if ( $product ) {
$product_name = esc_html( wp_strip_all_tags( $product->get_formatted_name() ) );
$linked_products .= "<option value='$product_id' selected='selected'>$product_name</option>";
}
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- variables are escaped above.
echo "
<div class='options_group subscription_linked_downloadable_products_section'>
<p class='form-field subscription_linked_downloadable_products'>
<label for='subscription-linked-downloadable-products'>$label</label>
<select
class='wc-product-search subscription-downloads-ids'
data-action='wc_subscription_linked_downloadable_products_search'
data-placeholder='$placeholder'
id='subscription-linked-downloadable-products'
multiple='multiple'
name='_subscription_linked_downloadable_products[]'
style='width: 50%;'
>
$linked_products
</select>
<span class='description'>$description</span>
$nonce_field
</p>
</div>
";
}
/**
* @param int $loop
* @param array $variation_data
* @param WP_Post $variation
*
* @return void
*/
public function variable_subscription_product_editor_ui( $loop, $variation_data, $variation ): void {
if ( ! $variation instanceof WP_Post ) {
wc_get_logger()->warning(
'Unable to add the downloadable products selector to the variation section of the product editor (we do not have a valid post object).',
array( 'backtrace' => true )
);
return;
}
$variation_id = (int) $variation->ID;
$label = esc_html__( 'Linked downloadable products', 'woocommerce-subscriptions' );
$linked_products = '';
$nonce_field = wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $variation_id, false, false );
$placeholder = esc_attr__( 'Select products', 'woocommerce-subscriptions' );
$tooltip = wc_help_tip( wc_sanitize_tooltip( __( 'Select simple and variable downloadable products that will be included with this subscription variation.', 'woocommerce-subscriptions' ) ) );
foreach ( WC_Subscription_Downloads::get_downloadable_products( $variation->ID ) as $product_id ) {
$product_id = absint( $product_id );
$product = wc_get_product( $product_id );
if ( $product ) {
$product_name = esc_html( wp_strip_all_tags( $product->get_formatted_name() ) );
$linked_products .= "<option value='$product_id' selected='selected'>$product_name</option>";
}
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- variables are escaped above.
echo "
<div class='variable_subscription_linked_downloadable_products show_if_variable-subscription' style='display: none'>
<p class='form-row form-field subscription_linked_downloadable_products'>
<label for='subscription-linked-downloadable-products'>$label</label>
$tooltip
<select
class='wc-product-search subscription-downloads-ids'
data-action='wc_subscription_linked_downloadable_products_search'
data-placeholder='$placeholder'
id='subscription-linked-downloadable-products'
multiple='multiple'
name='_subscription_linked_downloadable_products_{$variation_id}[]'
style='width: 100%;'
>
$linked_products
</select>
$nonce_field
</p>
</div>
";
}
/**
* Search orders from subscription product ID.
*
@ -166,9 +430,7 @@ class WC_Subscription_Downloads_Products {
$orders[] = $order->id;
}
$orders = apply_filters( 'woocommerce_subscription_downloads_get_orders', $orders, $subscription_product_id );
return $orders;
return apply_filters( 'woocommerce_subscription_downloads_get_orders', $orders, $subscription_product_id );
}
/**
@ -183,40 +445,93 @@ class WC_Subscription_Downloads_Products {
protected function revoke_access_to_download( $download_id, $product_id, $order_id ) {
global $wpdb;
$wpdb->query( $wpdb->prepare( "
DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE order_id = %d AND product_id = %d AND download_id = %s;
", $order_id, $product_id, $download_id ) );
$wpdb->query(
$wpdb->prepare(
"
DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE order_id = %d AND product_id = %d AND download_id = %s;
",
$order_id,
$product_id,
$download_id
)
);
do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id );
}
/**
* Update subscription downloads table and orders.
* Update subscription downloads table and orders according in respect to the described relationship between a
* regular product and zero-to-many regular subscription products.
*
* @param int $product_id
* @param array $subscriptions
* @param int $product_id The downloadable product ID.
* @param array $subscriptions Subscription product IDs.
*
* @return void
*/
protected function update_subscription_downloads( $product_id, $subscriptions ) {
$current = array_map( 'intval', WC_Subscription_Downloads::get_subscriptions( $product_id ) );
$subscriptions = array_map( 'intval', (array) $subscriptions );
sort( $current );
sort( $subscriptions );
$to_delete = array_diff( $current, $subscriptions );
$to_create = array_diff( $subscriptions, $current );
$this->delete_relationships( $to_delete, array( $product_id ) );
$this->create_relationships( $to_create, array( $product_id ) );
}
/**
* Update subscription downloads table and orders according in respect to the described relationship between a
* subscription product and zero-to-many regular products.
*
* @param int $subscription_product_id Subscription product ID.
* @param int[] $new_ids IDs for downloadable products that should be associated with the subscription product.
*
* @return void
*/
private function update_subscription_products( int $subscription_product_id, array $new_ids ): void {
$existing_ids = array_map( 'intval', WC_Subscription_Downloads::get_downloadable_products( $subscription_product_id ) );
$new_ids = array_map( 'intval', $new_ids );
sort( $existing_ids );
sort( $new_ids );
$to_delete = array_diff( $existing_ids, $new_ids );
$to_create = array_diff( $new_ids, $existing_ids );
$this->delete_relationships( array( $subscription_product_id ), $to_delete );
$this->create_relationships( array( $subscription_product_id ), $to_create );
}
/**
* Deletes relationships that exist between any of the supplied subscription IDs and any of the supplied product
* IDs.
*
* The most common use case will be to supply a single subscription ID and one-or-more product IDs, or else the
* inverse.
*
* @param int[] $subscription_ids
* @param int[] $product_ids
*
* @return void
*/
private function delete_relationships( array $subscription_ids, array $product_ids ): void {
global $wpdb;
if ( version_compare( WC_VERSION, '3.0', '<' ) && ! empty( $subscriptions ) ) {
$subscriptions = explode( ',', $subscriptions );
}
foreach ( $product_ids as $product_id ) {
$product_id = (int) $product_id;
$current = WC_Subscription_Downloads::get_subscriptions( $product_id );
foreach ( $subscription_ids as $subscription_id ) {
$subscription_id = (int) $subscription_id;
// Delete items.
$delete_ids = array_diff( $current, $subscriptions );
if ( $delete_ids ) {
foreach ( $delete_ids as $delete ) {
$wpdb->delete(
$wpdb->prefix . 'woocommerce_subscription_downloads',
array(
'product_id' => $product_id,
'subscription_id' => $delete,
'subscription_id' => $subscription_id,
),
array(
'%d',
@ -224,10 +539,10 @@ 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();
$orders = $this->get_orders( $subscription_id );
foreach ( $orders as $order_id ) {
$product = wc_get_product( $product_id );
$downloads = $product->get_downloads();
// Adds the downloadable files to the order/subscription.
foreach ( array_keys( $downloads ) as $download_id ) {
@ -236,16 +551,35 @@ class WC_Subscription_Downloads_Products {
}
}
}
}
// Add items.
$add_ids = array_diff( $subscriptions, $current );
if ( $add_ids ) {
foreach ( $add_ids as $add ) {
$wpdb->insert(
$wpdb->prefix . 'woocommerce_subscription_downloads',
/**
* Revoke download permissions for a product across all related subscriptions.
*
* @param int $product_id Product ID.
*
* @return void
*/
private function revoke_permissions_for_product( $product_id ) {
global $wpdb;
$subscription_ids = WC_Subscription_Downloads::get_subscriptions( $product_id );
if ( empty( $subscription_ids ) ) {
return;
}
foreach ( $subscription_ids as $subscription_id ) {
$orders = $this->get_orders( $subscription_id );
foreach ( $orders as $order_id ) {
// Delete ALL permissions for this product+order combination.
// This ensures that when files change, old permissions with different download_ids are removed.
$wpdb->delete(
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
array(
'product_id' => $product_id,
'subscription_id' => $add,
'order_id' => $order_id,
'product_id' => $product_id,
),
array(
'%d',
@ -253,23 +587,105 @@ class WC_Subscription_Downloads_Products {
)
);
$_orders = $this->get_orders( $add );
foreach ( $_orders as $order_id ) {
$order = wc_get_order( $order_id );
do_action( 'woocommerce_revoke_access_to_product_download', $product_id, $order_id );
}
}
}
if ( ! is_a( $order, 'WC_Subscription' ) ) {
// avoid adding permissions to orders and it's
// subscription for the same user, causing duplicates
// to show up
continue;
}
/**
* Grant download permissions for a product across all related subscriptions.
*
* @param int $product_id Product ID.
*
* @return void
*/
private function grant_permissions_for_product( $product_id ) {
$subscription_product_ids = WC_Subscription_Downloads::get_subscriptions( $product_id );
$product = wc_get_product( $product_id );
$_product = wc_get_product( $product_id );
$downloads = version_compare( WC_VERSION, '3.0', '<' ) ? $_product->get_files() : $_product->get_downloads();
if ( empty( $subscription_product_ids ) || ! $product ) {
return;
}
// Adds the downloadable files to the order/subscription.
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product_id, $order );
$downloads = $product->get_downloads();
foreach ( $subscription_product_ids as $subscription_id ) {
$orders = $this->get_orders( $subscription_id );
foreach ( $orders as $order_id ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, 'WC_Subscription' ) ) {
continue;
}
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product_id, $order );
}
}
}
}
/**
* Adds relationships between the specified subscription and product IDs.
*
* The most common use case will be to supply a single subscription ID and one-or-more product IDs, or else the
* inverse.
*
* @param int[] $subscription_ids
* @param int[] $product_ids
*
* @return void
*/
private function create_relationships( array $subscription_ids, array $product_ids ): void {
global $wpdb;
foreach ( $product_ids as $product_id ) {
$product_id = (int) $product_id;
$product = wc_get_product( $product_id );
// Check if product has public status.
$has_public_status = false;
if ( $product ) {
$status_object = get_post_status_object( $product->get_status() );
$has_public_status = $status_object && $status_object->public;
}
foreach ( $subscription_ids as $subscription_id ) {
$subscription_id = (int) $subscription_id;
$wpdb->insert(
$wpdb->prefix . 'woocommerce_subscription_downloads',
array(
'product_id' => $product_id,
'subscription_id' => $subscription_id,
),
array(
'%d',
'%d',
)
);
// Only grant download permissions if product has public status.
if ( $has_public_status ) {
$orders = $this->get_orders( $subscription_id );
foreach ( $orders as $order_id ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, 'WC_Subscription' ) ) {
// avoid adding permissions to orders and it's
// subscription for the same user, causing duplicates
// to show up
continue;
}
$product = wc_get_product( $product_id );
$downloads = $product->get_downloads();
// Adds the downloadable files to the order/subscription.
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product_id, $order );
}
}
}
}
@ -284,41 +700,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 );
}
/**
* Save variable product data.
*
* @param int $variation_id
* @param int $index
*
* @return void
*/
public function save_variation_product_data( $variation_id, $index ) {
if ( ! isset( $_POST['variable_is_downloadable'][ $index ] ) ) {
return;
}
$subscriptions = isset( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ? wc_clean( wp_unslash( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ) : array();
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( $product_id, $subscription_downloads_ids );
}
/**
@ -382,4 +771,45 @@ class WC_Subscription_Downloads_Products {
return (string) wc_get_formatted_variation( $product_variation, true );
}
/**
* Deprecated, do not use. Previously took care of saving product data for variations.
*
* @deprecated 8.3.0
*
* @param int $variation_id
* @param int $index
*
* @return void
*/
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
public function save_variation_product_data( $variation_id, $index ) {
wc_deprecated_function( __METHOD__, '8.3.0', __CLASS__ . '::handle_product_variation_save' );
}
/**
* Deprecated, do not use. Previously took care of saving product data.
*
* @deprecated 8.3.0
*
* @param int $subscription_product_id
* @param int|null $index
*
* @return void
*/
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
public function save_subscription_product_data( int $subscription_product_id, ?int $index = null ) {
wc_deprecated_function( __METHOD__, '8.3.0', __CLASS__ . '::handle_product_save' );
}
/**
* Deprecated, do not use. Previously set up assets for the Subscription Downloads extension.
*
* @deprecated 8.3.0
*
* @return void
*/
public function scripts() {
wc_deprecated_function( __METHOD__, '8.3.0' );
}
}

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.
*
@ -25,8 +60,8 @@ class WC_Subscription_Downloads_Settings {
'id' => WC_Subscriptions_Admin::$option_prefix . '_downloads_settings',
),
array(
'name' => __( 'Enable product linking to subscriptions', 'woocommerce-subscriptions' ),
'desc' => __( 'Allow simple and variable downloadable products to be included with subscription products.', 'woocommerce-subscriptions' ),
'name' => __( 'Enable downloadable file sharing', 'woocommerce-subscriptions' ),
'desc' => __( 'Allow downloadable files from simple and variable products to be shared with subscription products so they are available to active subscribers.', 'woocommerce-subscriptions' ),
'id' => WC_Subscriptions_Admin::$option_prefix . '_enable_downloadable_file_linking',
'default' => 'no',
'type' => 'checkbox',

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

@ -33,32 +33,8 @@ class WCSG_Admin_Welcome_Announcement {
$screen = get_current_screen();
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/gifting-welcome-announcement.asset.php' );
$script_asset = file_exists( $script_asset_path )
? require $script_asset_path
: array(
'dependencies' => array(
'react',
'wc-blocks-checkout',
'wc-price-format',
'wc-settings',
'wp-element',
'wp-i18n',
'wp-plugins',
),
'version' => WC_Subscriptions::$version,
);
wp_enqueue_script(
'wcs-gifting-welcome-announcement',
plugins_url( '/build/gifting-welcome-announcement.js', WC_Subscriptions::$plugin_file ),
$script_asset['dependencies'],
$script_asset['version'],
true
);
wp_localize_script(
'wcs-gifting-welcome-announcement',
'wcs-admin',
'wcsGiftingSettings',
array(
'imagesPath' => plugins_url( '/assets/images', WC_Subscriptions::$plugin_file ),
@ -68,19 +44,6 @@ class WCSG_Admin_Welcome_Announcement {
'isSubscriptionsListing' => 'woocommerce_page_wc-orders--shop_subscription' === $screen->id,
)
);
wp_enqueue_style(
'wcs-gifting-welcome-announcement',
plugins_url( '/build/style-gifting-welcome-announcement.css', WC_Subscriptions::$plugin_file ),
array( 'wp-components' ),
$script_asset['version']
);
wp_set_script_translations(
'wcs-gifting-welcome-announcement',
'woocommerce-subscriptions',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'languages'
);
}
/**

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

@ -1,4 +1,7 @@
<?php
use Automattic\WooCommerce_Subscriptions\Internal\Utilities\Request;
/**
* A class to make it possible to switch between different subscriptions (i.e. upgrade/downgrade a subscription)
*
@ -44,6 +47,9 @@ class WC_Subscriptions_Switcher {
// Add the "Switch" button to the View Subscription table
add_action( 'woocommerce_order_item_meta_end', array( __CLASS__, 'print_switch_link' ), 10, 3 );
// Add hidden form inputs for AJAX add-to-cart compatibility during switches
add_action( 'woocommerce_before_add_to_cart_button', array( __CLASS__, 'add_switch_hidden_inputs' ) );
// We need to create subscriptions on checkout and want to do it after almost all other extensions have added their products/items/fees
add_action( 'woocommerce_checkout_order_processed', array( __CLASS__, 'process_checkout' ), 50, 2 );
@ -179,22 +185,26 @@ class WC_Subscriptions_Switcher {
public static function subscription_switch_handler() {
global $post;
// If the current user doesn't own the subscription, remove the query arg from the URL
if ( isset( $_GET['switch-subscription'] ) && isset( $_GET['item'] ) ) {
$switch_subscription_id = Request::get_var( 'switch-subscription' );
$item_id = Request::get_var( 'item' );
$subscription = wcs_get_subscription( absint( $_GET['switch-subscription'] ) );
$line_item = $subscription ? wcs_get_order_item( absint( $_GET['item'] ), $subscription ) : false;
$nonce = ! empty( $_GET['_wcsnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wcsnonce'] ) ) : false;
// If the current user doesn't own the subscription, remove the query arg from the URL
if ( $switch_subscription_id && $item_id ) {
$subscription = wcs_get_subscription( absint( $switch_subscription_id ) );
$line_item = $subscription ? wcs_get_order_item( absint( $item_id ), $subscription ) : false;
$nonce = Request::get_var( '_wcsnonce' );
$nonce = $nonce ? sanitize_text_field( wp_unslash( $nonce ) ) : false;
// Visiting a switch link for someone elses subscription or if the switch link doesn't contain a valid nonce
if ( ! is_object( $subscription ) || empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wcs_switch_request' ) || empty( $line_item ) || ! self::can_item_be_switched_by_user( $line_item, $subscription ) ) {
wp_safe_redirect( remove_query_arg( array( 'switch-subscription', 'auto-switch', 'item', '_wcsnonce' ) ) );
exit();
Request::redirect( remove_query_arg( array( 'switch-subscription', 'auto-switch', 'item', '_wcsnonce' ) ) );
return;
} else {
if ( isset( $_GET['auto-switch'] ) ) {
if ( Request::get_var( 'auto-switch' ) ) {
$switch_message = __( 'You have a subscription to this product. Choosing a new subscription will replace your existing subscription.', 'woocommerce-subscriptions' );
} else {
$switch_message = __( 'Choose a new subscription.', 'woocommerce-subscriptions' );
@ -239,8 +249,8 @@ class WC_Subscriptions_Switcher {
if ( $removed_item_count > 0 ) {
wc_add_notice( _n( 'Your cart contained an invalid subscription switch request. It has been removed.', 'Your cart contained invalid subscription switch requests. They have been removed.', $removed_item_count, 'woocommerce-subscriptions' ), 'error' );
wp_safe_redirect( wc_get_cart_url() );
exit();
Request::redirect( wc_get_cart_url() );
return;
}
} elseif ( is_product() && $product = wc_get_product( $post ) ) { // Automatically initiate the switch process for limited variable subscriptions
@ -308,8 +318,8 @@ class WC_Subscriptions_Switcher {
}
if ( apply_filters( 'wcs_initiate_auto_switch', self::can_item_be_switched_by_user( $item, $subscription ), $item, $subscription ) ) {
wp_safe_redirect( add_query_arg( 'auto-switch', 'true', self::get_switch_url( $item_id, $item, $subscription ) ) );
exit;
Request::redirect( add_query_arg( 'auto-switch', 'true', self::get_switch_url( $item_id, $item, $subscription ) ) );
return;
}
}
}
@ -560,6 +570,30 @@ class WC_Subscriptions_Switcher {
echo wp_kses( apply_filters( 'woocommerce_subscriptions_switch_link', $switch_link, $item_id, $item, $subscription ), array( 'a' => array( 'href' => array(), 'title' => array(), 'class' => array() ) ) ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
}
/**
* Add hidden form inputs for subscription switch parameters.
*
* When a customer is switching subscriptions, the switch parameters are passed via URL query arguments.
* This method outputs them as hidden form inputs so they're included when AJAX add-to-cart plugins
* serialize and submit the form via POST.
*
* @since 8.3.0
*/
public static function add_switch_hidden_inputs() {
$switch_subscription_id = Request::get_var( 'switch-subscription' );
$item_id = Request::get_var( 'item' );
$nonce = Request::get_var( '_wcsnonce' );
// Only output if we're in a switch context with a valid nonce
if ( ! $switch_subscription_id || ! $item_id || ! $nonce ) {
return;
}
echo '<input type="hidden" name="switch-subscription" value="' . esc_attr( $switch_subscription_id ) . '" />';
echo '<input type="hidden" name="item" value="' . esc_attr( $item_id ) . '" />';
echo '<input type="hidden" name="_wcsnonce" value="' . esc_attr( $nonce ) . '" />';
}
/**
* The link for switching a subscription - the product page for variable subscriptions, or grouped product page for grouped subscriptions.
*
@ -1282,7 +1316,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();
@ -1356,21 +1416,25 @@ class WC_Subscriptions_Switcher {
try {
if ( ! isset( $_GET['switch-subscription'] ) ) {
$switch_subscription_id = Request::get_var( 'switch-subscription' );
if ( ! $switch_subscription_id ) {
return $is_valid;
}
if ( empty( $_GET['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wcsnonce'] ) ), 'wcs_switch_request' ) ) {
$nonce = Request::get_var( '_wcsnonce' );
if ( empty( $nonce ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), 'wcs_switch_request' ) ) {
return false;
}
$subscription = wcs_get_subscription( absint( $_GET['switch-subscription'] ) );
$subscription = wcs_get_subscription( absint( $switch_subscription_id ) );
if ( ! $subscription ) {
throw new Exception( __( 'The subscription may have been deleted.', 'woocommerce-subscriptions' ) );
}
$item_id = absint( $_GET['item'] );
$item_id = absint( Request::get_var( 'item' ) );
$item = wcs_get_order_item( $item_id, $subscription );
// Prevent switching to non-subscription product
@ -1442,11 +1506,13 @@ class WC_Subscriptions_Switcher {
public static function set_switch_details_in_cart( $cart_item_data, $product_id, $variation_id ) {
try {
if ( ! isset( $_GET['switch-subscription'] ) ) {
$switch_subscription_id = Request::get_var( 'switch-subscription' );
if ( ! $switch_subscription_id ) {
return $cart_item_data;
}
$subscription = wcs_get_subscription( absint( $_GET['switch-subscription'] ) );
$subscription = wcs_get_subscription( absint( $switch_subscription_id ) );
if ( ! $subscription ) {
throw new Exception( __( 'The subscription may have been deleted.', 'woocommerce-subscriptions' ) );
@ -1456,11 +1522,11 @@ class WC_Subscriptions_Switcher {
if ( ! current_user_can( 'switch_shop_subscription', $subscription->get_id() ) ) {
wc_add_notice( __( 'You can not switch this subscription. It appears you do not own the subscription.', 'woocommerce-subscriptions' ), 'error' );
WC()->cart->empty_cart( true );
wp_safe_redirect( get_permalink( $product_id ) );
exit();
Request::redirect( get_permalink( $product_id ) );
return;
}
$item = wcs_get_order_item( absint( $_GET['item'] ), $subscription );
$item = wcs_get_order_item( absint( Request::get_var( 'item' ) ), $subscription );
// Else it's a valid switch
$product = wc_get_product( $item['product_id'] );
@ -1469,7 +1535,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 ) );
}
}
@ -1486,7 +1578,7 @@ class WC_Subscriptions_Switcher {
$cart_item_data['subscription_switch'] = array(
'subscription_id' => $subscription->get_id(),
'item_id' => absint( $_GET['item'] ),
'item_id' => absint( Request::get_var( 'item' ) ),
'next_payment_timestamp' => $next_payment_timestamp,
'upgraded_or_downgraded' => '',
);
@ -1497,8 +1589,8 @@ class WC_Subscriptions_Switcher {
wc_add_notice( __( 'There was an error locating the switch details.', 'woocommerce-subscriptions' ), 'error' );
WC()->cart->empty_cart( true );
wp_safe_redirect( get_permalink( wc_get_page_id( 'cart' ) ) );
exit();
Request::redirect( get_permalink( wc_get_page_id( 'cart' ) ) );
return;
}
}

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,102 @@
<?php
namespace Automattic\WooCommerce_Subscriptions\Internal\Utilities;
/**
* Utilities for handling request-related operations, including test-safe redirects and exits.
*
* @internal This class may be modified, moved or removed in future releases.
*/
class Request {
/**
* Cached POST input variables.
*
* @var array|null
*/
private static ?array $post_vars = null;
/**
* Cached GET input variables.
*
* @var array|null
*/
private static ?array $url_vars = null;
/**
* Exit unless running in test environment.
*
* This method allows tests to run without terminating the PHP process,
* enabling proper code coverage collection and test assertions.
* In production, this behaves exactly like exit().
*
* @since 8.3.0
* @return void
*/
public static function exit() {
if ( defined( 'WCS_ENVIRONMENT_TYPE' ) && 'tests' === WCS_ENVIRONMENT_TYPE ) {
return;
}
exit();
}
/**
* Safe redirect that works in test environment.
*
* In production, this performs a normal wp_safe_redirect() and exits by default.
* In tests, it stores the redirect information in a global variable for assertions
* instead of attempting to send headers (which would fail).
*
* @since 8.3.0
* @param string $location The URL to redirect to.
* @param int $status HTTP status code (default 302).
* @param bool $should_exit Whether to exit after setting redirect headers (default true).
* @return void
*/
public static function redirect( $location, $status = 302, $should_exit = true ) {
if ( defined( 'WCS_ENVIRONMENT_TYPE' ) && 'tests' === WCS_ENVIRONMENT_TYPE ) {
// Store redirect info for test assertions but don't actually redirect
$GLOBALS['wcs_test_redirect'] = array(
'location' => $location,
'status' => $status,
);
return;
}
wp_safe_redirect( $location, $status );
if ( $should_exit ) {
self::exit();
}
}
/**
* Supplies the value of the POST or URL query parameter matching $key, or else returns $default.
*
* Essentially, this is an alternative to inspecting the $_REQUEST super-global and is intended for cases where we
* are interested in a key:value pair, regardless of whether it was sent as a post var or URL query var.
*
* Its advantages are that it only ever examines the POST and GET inputs (POST taking priority, if both contain the
* same key): cookies are always ignored. It also looks directly at the inputs, instead of using the $_POST or $_GET
* superglobals (which can be manipulated).
*
* @since 8.3.0
*
* @param string $key The key to look up.
* @param mixed $default_value The value to return if the key is not found. Defaults to null.
*
* @return mixed
*/
public static function get_var( string $key, $default_value = null ) {
if ( null === self::$post_vars ) {
self::$post_vars = filter_input_array( INPUT_POST ) ?? array();
self::$url_vars = filter_input_array( INPUT_GET ) ?? array();
}
if ( isset( self::$post_vars[ $key ] ) ) {
return self::$post_vars[ $key ];
} elseif ( isset( self::$url_vars[ $key ] ) ) {
return self::$url_vars[ $key ];
}
return $default_value;
}
}

View File

@ -2,6 +2,8 @@
/**
* Outputs a subscription variation's pricing fields for WooCommerce 2.3+
*
* You may also be interested in assets/js/admin/admin.js, which dynamically relocates some of these fields.
*
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.12
*
* @var int $loop

View File

@ -8,9 +8,9 @@
*/
defined( 'ABSPATH' ) || exit;
$display_heading = true;
foreach ( WC()->cart->get_coupons() as $code => $coupon ) {
$display_heading = true;
foreach ( $recurring_carts as $recurring_cart_key => $recurring_cart ) {
foreach ( $recurring_cart->get_coupons() as $recurring_code => $recurring_coupon ) {
if ( $recurring_code !== $code ) {

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

@ -12,6 +12,7 @@ return array(
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Orders' => $baseDir . '/src/Internal/Telemetry/Orders.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Products' => $baseDir . '/src/Internal/Telemetry/Products.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Subscriptions' => $baseDir . '/src/Internal/Telemetry/Subscriptions.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Utilities\\Request' => $baseDir . '/src/Internal/Utilities/Request.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Utilities\\Scheduled_Actions' => $baseDir . '/src/Internal/Utilities/Scheduled_Actions.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Composer\\Installers\\AglInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AglInstaller.php',
@ -138,6 +139,7 @@ return array(
'WCSG_Template_Loader' => $baseDir . '/includes/gifting/class-wcsg-template-loader.php',
'WCS_Gifting' => $baseDir . '/includes/gifting/class-wcs-gifting.php',
'WC_Subscription_Downloads' => $baseDir . '/includes/downloads/class-wc-subscription-downloads.php',
'WC_Subscription_Downloads_Admin_Welcome_Announcement' => $baseDir . '/includes/downloads/class-wc-subscription-downloads-admin-welcome-announcement.php',
'WC_Subscription_Downloads_Ajax' => $baseDir . '/includes/downloads/class-wc-subscription-downloads-ajax.php',
'WC_Subscription_Downloads_Install' => $baseDir . '/includes/downloads/class-wc-subscription-downloads-install.php',
'WC_Subscription_Downloads_Order' => $baseDir . '/includes/downloads/class-wc-subscription-downloads-order.php',

View File

@ -35,6 +35,7 @@ class ComposerStaticInitcb45de1ca955f89ec737c442c7cf101c
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Orders' => __DIR__ . '/../..' . '/src/Internal/Telemetry/Orders.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Products' => __DIR__ . '/../..' . '/src/Internal/Telemetry/Products.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Subscriptions' => __DIR__ . '/../..' . '/src/Internal/Telemetry/Subscriptions.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Utilities\\Request' => __DIR__ . '/../..' . '/src/Internal/Utilities/Request.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Utilities\\Scheduled_Actions' => __DIR__ . '/../..' . '/src/Internal/Utilities/Scheduled_Actions.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Composer\\Installers\\AglInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AglInstaller.php',
@ -161,6 +162,7 @@ class ComposerStaticInitcb45de1ca955f89ec737c442c7cf101c
'WCSG_Template_Loader' => __DIR__ . '/../..' . '/includes/gifting/class-wcsg-template-loader.php',
'WCS_Gifting' => __DIR__ . '/../..' . '/includes/gifting/class-wcs-gifting.php',
'WC_Subscription_Downloads' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads.php',
'WC_Subscription_Downloads_Admin_Welcome_Announcement' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads-admin-welcome-announcement.php',
'WC_Subscription_Downloads_Ajax' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads-ajax.php',
'WC_Subscription_Downloads_Install' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads-install.php',
'WC_Subscription_Downloads_Order' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads-order.php',

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.3.0',
'version' => 'dev-release/8.3.0',
'reference' => '38b39c285d0bae952d5746ba460ae914e4ac0f60',
'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.3.0',
'version' => 'dev-release/8.3.0',
'reference' => '38b39c285d0bae952d5746ba460ae914e4ac0f60',
'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.3.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.3
* 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.3.0'; // WRCS: DEFINED_VERSION.
/** @var string */
public static $wc_minimum_supported_version = '7.7';