Updates to 8.3.0

This commit is contained in:
WooCommerce 2026-01-08 10:21:29 +00:00
parent bfe7fb609d
commit 56e03c0544
37 changed files with 5052 additions and 427 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

@ -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

@ -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 ) {

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' => '841d6ab2fe0080f15a21');
<?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,11 +1,17 @@
*** 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.

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();
@ -300,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

@ -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

@ -124,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 );
}
/**
@ -134,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

@ -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

@ -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,8 +212,8 @@ 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
@ -71,17 +221,18 @@ class WC_Subscription_Downloads_Products {
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
@ -94,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.
*
@ -164,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 );
}
/**
@ -181,37 +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;
$subscriptions = (array) $subscriptions;
$current = WC_Subscription_Downloads::get_subscriptions( $product_id );
foreach ( $product_ids as $product_id ) {
$product_id = (int) $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',
@ -219,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 = $_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 ) {
@ -231,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',
@ -248,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 = $_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 );
}
}
}
}
@ -289,26 +710,6 @@ class WC_Subscription_Downloads_Products {
$this->update_subscription_downloads( $product_id, $subscription_downloads_ids );
}
/**
* 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;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_download_ids = isset( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ? wc_clean( wp_unslash( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ) : array();
$subscription_download_ids = array_filter( $subscription_download_ids ); // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- $subscription_download_ids are already passed through wc_clean() and wp_unslash().
$this->update_subscription_downloads( $variation_id, $subscription_download_ids );
}
/**
* Save subscriptions information when duplicating a product.
*
@ -370,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

@ -19,11 +19,6 @@ class WC_Subscription_Downloads_Settings {
* @since 8.0.0
*/
public static function add_notice_about_bundled_feature() {
if ( ! Constants::is_true( 'WCS_ALLOW_SUBSCRIPTION_DOWNLOADS' ) ) {
// TODO: remove this conditional as soon as we are ready to make subscription downloads functionality live.
return;
}
$screen = get_current_screen();
if ( ! $screen ) {
return false;
@ -65,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

@ -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

@ -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.
*
@ -1382,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
@ -1468,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' ) );
@ -1482,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'] );
@ -1538,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' => '',
);
@ -1549,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;
}
}

View File

@ -1,15 +1,15 @@
# Copyright (C) 2025 WooCommerce
# Copyright (C) 2026 WooCommerce
# This file is distributed under the GNU General Public License v3.0.
msgid ""
msgstr ""
"Project-Id-Version: WooCommerce Subscriptions 8.2.1\n"
"Project-Id-Version: WooCommerce Subscriptions 8.3.0\n"
"Report-Msgid-Bugs-To: https://woocommerce.com/contact-us\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2025-12-22T21:15:57+00:00\n"
"POT-Creation-Date: 2026-01-06T17:54:09+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.12.0\n"
"X-Domain: woocommerce-subscriptions\n"
@ -889,7 +889,7 @@ msgstr ""
msgid "Order already has subscriptions associated with it."
msgstr ""
#: includes/api/class-wc-rest-subscriptions-settings-option-controller.php:83
#: includes/api/class-wc-rest-subscriptions-settings-option-controller.php:84
msgid "Invalid value type; must be either boolean or array"
msgstr ""
@ -1123,26 +1123,26 @@ msgid "%1$sWooCommerce Subscriptions is inactive.%2$s This version of Subscripti
msgstr ""
#. translators: 1-2: opening/closing <b> tags, 3: Subscriptions version.
#: includes/class-wc-subscriptions-plugin.php:70
#: includes/class-wc-subscriptions-plugin.php:71
#, php-format
msgid "%1$sWarning!%2$s We can see the %1$sWooCommerce Subscriptions Early Renewal%2$s plugin is active. Version %3$s of %1$sWooCommerce Subscriptions%2$s comes with that plugin's functionality packaged into the core plugin. Please deactivate WooCommerce Subscriptions Early Renewal to avoid any conflicts."
msgstr ""
#: includes/class-wc-subscriptions-plugin.php:74
#: includes/class-wc-subscriptions-plugin.php:75
msgid "Installed Plugins"
msgstr ""
#. translators: $1-$2: opening and closing <strong> tags, $3-$4: opening and closing <em> tags.
#: includes/class-wc-subscriptions-plugin.php:216
#: includes/class-wc-subscriptions-plugin.php:217
#, php-format
msgid "%1$sWooCommerce Subscriptions Installed%2$s &#8211; %3$sYou're ready to start selling subscriptions!%4$s"
msgstr ""
#: includes/class-wc-subscriptions-plugin.php:234
#: includes/class-wc-subscriptions-plugin.php:235
msgid "Add a Subscription Product"
msgstr ""
#: includes/class-wc-subscriptions-plugin.php:235
#: includes/class-wc-subscriptions-plugin.php:236
#: includes/core/class-wc-subscriptions-core-plugin.php:580
#: includes/core/upgrades/templates/wcs-about-2-0.php:35
#: includes/core/upgrades/templates/wcs-about.php:34
@ -1407,7 +1407,7 @@ msgid "Virtual"
msgstr ""
#: includes/core/admin/class-wc-subscriptions-admin.php:296
#: templates/admin/html-variation-price.php:27
#: templates/admin/html-variation-price.php:29
msgid "Choose the subscription price, billing interval and period."
msgstr ""
@ -1421,7 +1421,7 @@ msgstr ""
#. translators: %s: currency symbol.
#. translators: placeholder is a currency symbol / code
#: includes/core/admin/class-wc-subscriptions-admin.php:313
#: templates/admin/html-variation-price.php:23
#: templates/admin/html-variation-price.php:25
#, php-format
msgid "Subscription price (%s)"
msgstr ""
@ -1430,7 +1430,7 @@ msgstr ""
#. translators: %s the formatted example price value.
#: includes/core/admin/class-wc-subscriptions-admin.php:318
#: includes/core/admin/class-wc-subscriptions-admin.php:360
#: templates/admin/html-variation-price.php:33
#: templates/admin/html-variation-price.php:35
#, php-format
msgctxt "example price"
msgid "e.g. %s"
@ -1447,7 +1447,7 @@ msgstr ""
#: includes/core/admin/class-wc-subscriptions-admin.php:344
#: includes/core/admin/class-wc-subscriptions-admin.php:516
#: templates/admin/html-variation-price.php:53
#: templates/admin/html-variation-price.php:55
msgid "Stop renewing after"
msgstr ""
@ -1457,19 +1457,19 @@ msgstr ""
#. translators: %s is a currency symbol / code
#: includes/core/admin/class-wc-subscriptions-admin.php:358
#: templates/admin/html-variation-price.php:69
#: templates/admin/html-variation-price.php:71
#, php-format
msgid "Sign-up fee (%s)"
msgstr ""
#: includes/core/admin/class-wc-subscriptions-admin.php:361
#: templates/admin/html-variation-price.php:72
#: templates/admin/html-variation-price.php:74
msgid "Optionally include an amount to be charged at the outset of the subscription. The sign-up fee will be charged immediately, even if the product has a free trial or the payment dates are synced."
msgstr ""
#: includes/core/admin/class-wc-subscriptions-admin.php:375
#: includes/core/class-wc-subscriptions-cart.php:2458
#: templates/admin/html-variation-price.php:79
#: templates/admin/html-variation-price.php:81
msgid "Free trial"
msgstr ""
@ -1480,22 +1480,22 @@ msgstr ""
#: includes/core/admin/class-wc-subscriptions-admin.php:403
#: includes/gifting/class-wcsg-admin.php:182
#: templates/admin/html-variation-price.php:107
#: templates/admin/html-variation-price.php:109
msgid "Gifting"
msgstr ""
#: includes/core/admin/class-wc-subscriptions-admin.php:407
#: templates/admin/html-variation-price.php:117
#: templates/admin/html-variation-price.php:119
msgid "Enabled"
msgstr ""
#: includes/core/admin/class-wc-subscriptions-admin.php:408
#: templates/admin/html-variation-price.php:118
#: templates/admin/html-variation-price.php:120
msgid "Disabled"
msgstr ""
#: includes/core/admin/class-wc-subscriptions-admin.php:411
#: templates/admin/html-variation-price.php:110
#: templates/admin/html-variation-price.php:112
msgid "Allow shoppers to purchase a subscription as a gift."
msgstr ""
@ -3932,13 +3932,13 @@ msgid "Never (charge the full recurring amount at sign-up)"
msgstr ""
#: includes/core/class-wc-subscriptions-synchroniser.php:226
#: includes/switching/class-wc-subscriptions-switcher.php:442
#: includes/switching/class-wc-subscriptions-switcher.php:452
msgctxt "when to prorate first payment / subscription length"
msgid "For Virtual Subscription Products Only"
msgstr ""
#: includes/core/class-wc-subscriptions-synchroniser.php:227
#: includes/switching/class-wc-subscriptions-switcher.php:443
#: includes/switching/class-wc-subscriptions-switcher.php:453
msgctxt "when to prorate first payment / subscription length"
msgid "For All Subscription Products"
msgstr ""
@ -5374,64 +5374,64 @@ msgstr ""
msgid "Customers with a subscription are excluded from this setting."
msgstr ""
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:307
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:306
msgid "A database upgrade is required to use Subscriptions. Upgrades from the previously installed version is no longer supported. You will need to install an older version of WooCommerce Subscriptions or WooCommerce Payments to proceed with the upgrade before you can use a newer version."
msgstr ""
#. translators: 1-2: opening/closing <strong> tags, 3: active version of Subscriptions, 4: current version of Subscriptions, 5-6: opening/closing tags linked to ticket form, 7-8: opening/closing tags linked to documentation.
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:369
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:368
#, php-format
msgid "%1$sWarning!%2$s It appears that you have downgraded %1$sWooCommerce Subscriptions%2$s from %3$s to %4$s. Downgrading the plugin in this way may cause issues. Please update to %3$s or higher, or %5$sopen a new support ticket%6$s for further assistance. %7$sLearn more &raquo;%8$s"
msgstr ""
#. translators: placeholder is a list of version numbers (e.g. "1.3 & 1.4 & 1.5")
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:508
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:507
#, php-format
msgid "Database updated to version %s"
msgstr ""
#. translators: placeholder is number of upgraded subscriptions
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:516
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:515
#, php-format
msgctxt "used in the subscriptions upgrader"
msgid "Marked %s subscription products as \"sold individually\"."
msgstr ""
#. translators: 1$: number of action scheduler hooks upgraded, 2$: "{execution_time}", will be replaced on front end with actual time
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:525
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:524
#, php-format
msgid "Migrated %1$s subscription related hooks to the new scheduler (in %2$s seconds)."
msgstr ""
#. translators: 1$: number of subscriptions upgraded, 2$: "{execution_time}", will be replaced on front end with actual time it took
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:537
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:536
#, php-format
msgid "Migrated %1$s subscriptions to the new structure (in %2$s seconds)."
msgstr ""
#. translators: placeholder is "{time_left}", will be replaced on front end with actual time
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:540
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:586
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:539
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:585
#, php-format
msgctxt "Message that gets sent to front end."
msgid "Estimated time left (minutes:seconds): %s"
msgstr ""
#. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, 4$: break tag
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:550
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:549
#, php-format
msgid "Unable to upgrade subscriptions.%4$sError: %1$s%4$sPlease refresh the page and try again. If problem persists, %2$scontact support%3$s."
msgstr ""
#. translators: placeholder is the number of subscriptions repaired
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:565
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:564
#, php-format
msgctxt "Repair message that gets sent to front end."
msgid "Repaired %d subscriptions with incorrect dates, line tax data or missing customer notes."
msgstr ""
#. translators: placeholder is number of subscriptions that were checked and did not need repairs. There's a space at the beginning!
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:571
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:570
#, php-format
msgctxt "Repair message that gets sent to front end."
msgid " %d other subscription was checked and did not need any repairs."
@ -5440,36 +5440,36 @@ msgstr[0] ""
msgstr[1] ""
#. translators: placeholder is "{execution_time}", which will be replaced on front end with actual time
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:575
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:574
#, php-format
msgctxt "Repair message that gets sent to front end."
msgid "(in %s seconds)"
msgstr ""
#. translators: $1: "Repaired x subs with incorrect dates...", $2: "X others were checked and no repair needed", $3: "(in X seconds)". Ordering for RTL languages.
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:578
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:577
#, php-format
msgctxt "The assembled repair message that gets sent to front end."
msgid "%1$s%2$s %3$s"
msgstr ""
#. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, 4$: break tag
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:597
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:596
#, php-format
msgctxt "Error message that gets sent to front end when upgrading Subscriptions"
msgid "Unable to repair subscriptions.%4$sError: %1$s%4$sPlease refresh the page and try again. If problem persists, %2$scontact support%3$s."
msgstr ""
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:805
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:804
msgid "Welcome to WooCommerce Subscriptions 2.1"
msgstr ""
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:805
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:804
msgid "About WooCommerce Subscriptions"
msgstr ""
#. translators: 1-2: opening/closing <strong> tags, 3-4: opening/closing tags linked to ticket form.
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:1043
#: includes/core/upgrades/class-wc-subscriptions-upgrader.php:1042
#, php-format
msgid "%1$sWarning!%2$s We discovered an issue in %1$sWooCommerce Subscriptions 2.3.0 - 2.3.2%2$s that may cause your subscription renewal order and customer subscription caches to contain invalid data. For information about how to update the cached data, please %3$sopen a new support ticket%4$s."
msgstr ""
@ -6457,35 +6457,53 @@ msgstr ""
msgid "Available downloads"
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-products.php:66
#: includes/downloads/class-wc-subscription-downloads-products.php:98
msgid "Included subscription products"
#: includes/downloads/class-wc-subscription-downloads-products.php:216
#: includes/downloads/class-wc-subscription-downloads-products.php:249
msgid "Linked subscription products"
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-products.php:68
#: includes/downloads/class-wc-subscription-downloads-products.php:101
#: includes/downloads/class-wc-subscription-downloads-products.php:218
#: includes/downloads/class-wc-subscription-downloads-products.php:252
msgid "Select subscriptions"
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-products.php:84
#: includes/downloads/class-wc-subscription-downloads-products.php:99
#: includes/downloads/class-wc-subscription-downloads-products.php:234
#: includes/downloads/class-wc-subscription-downloads-products.php:250
msgid "Select subscription products that will include this downloadable product."
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-settings.php:41
#: includes/downloads/class-wc-subscription-downloads-products.php:291
msgid "Select simple and variable downloadable products that will be included with this subscription product."
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-products.php:292
#: includes/downloads/class-wc-subscription-downloads-products.php:348
msgid "Linked downloadable products"
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-products.php:295
#: includes/downloads/class-wc-subscription-downloads-products.php:351
msgid "Select products"
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-products.php:352
msgid "Select simple and variable downloadable products that will be included with this subscription variation."
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-settings.php:36
msgid "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."
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-settings.php:63
#: includes/downloads/class-wc-subscription-downloads-settings.php:58
msgid "Downloads"
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-settings.php:68
msgid "Enable product linking to subscriptions"
#: includes/downloads/class-wc-subscription-downloads-settings.php:63
msgid "Enable downloadable file sharing"
msgstr ""
#: includes/downloads/class-wc-subscription-downloads-settings.php:69
msgid "Allow simple and variable downloadable products to be included with subscription products."
#: includes/downloads/class-wc-subscription-downloads-settings.php:64
msgid "Allow downloadable files from simple and variable products to be shared with subscription products so they are available to active subscribers."
msgstr ""
#: includes/early-renewal/class-wcs-cart-early-renewal.php:74
@ -7234,217 +7252,217 @@ msgstr ""
msgid "Automatic renewal payment failed"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:198
#: includes/switching/class-wc-subscriptions-switcher.php:208
msgid "You have a subscription to this product. Choosing a new subscription will replace your existing subscription."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:200
#: includes/switching/class-wc-subscriptions-switcher.php:210
msgid "Choose a new subscription."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:240
#: includes/switching/class-wc-subscriptions-switcher.php:1231
#: includes/switching/class-wc-subscriptions-switcher.php:250
#: includes/switching/class-wc-subscriptions-switcher.php:1265
msgid "Your cart contained an invalid subscription switch request. It has been removed."
msgid_plural "Your cart contained invalid subscription switch requests. They have been removed."
msgstr[0] ""
msgstr[1] ""
#: includes/switching/class-wc-subscriptions-switcher.php:282
#: includes/switching/class-wc-subscriptions-switcher.php:292
msgid "You have already subscribed to this product and it is limited to one per customer. You can not purchase the product again."
msgstr ""
#. translators: 1$: is the "You have already subscribed to this product" notice, 2$-4$: opening/closing link tags, 3$: an order number
#: includes/switching/class-wc-subscriptions-switcher.php:291
#: includes/switching/class-wc-subscriptions-switcher.php:301
#, php-format
msgid "%1$s Complete payment on %2$sOrder %3$s%4$s to be able to change your subscription."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:387
#: includes/switching/class-wc-subscriptions-switcher.php:397
msgid "Switching"
msgstr ""
#. translators: placeholders are opening and closing link tags
#: includes/switching/class-wc-subscriptions-switcher.php:390
#: includes/switching/class-wc-subscriptions-switcher.php:400
#, php-format
msgid "Allow subscribers to switch (upgrade or downgrade) between different subscriptions. %1$sLearn more%2$s."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:398
#: includes/switching/class-wc-subscriptions-switcher.php:408
msgid "Prorate Recurring Payment"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:399
#: includes/switching/class-wc-subscriptions-switcher.php:409
msgid "When switching to a subscription with a different recurring payment or billing period, should the price paid for the existing billing period be prorated when switching to the new subscription?"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:407
#: includes/switching/class-wc-subscriptions-switcher.php:441
#: includes/switching/class-wc-subscriptions-switcher.php:417
#: includes/switching/class-wc-subscriptions-switcher.php:451
msgctxt "when to allow a setting"
msgid "Never"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:408
#: includes/switching/class-wc-subscriptions-switcher.php:418
msgctxt "when to prorate recurring fee when switching"
msgid "For Upgrades of Virtual Subscription Products Only"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:409
#: includes/switching/class-wc-subscriptions-switcher.php:419
msgctxt "when to prorate recurring fee when switching"
msgid "For Upgrades of All Subscription Products"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:410
#: includes/switching/class-wc-subscriptions-switcher.php:420
msgctxt "when to prorate recurring fee when switching"
msgid "For Upgrades & Downgrades of Virtual Subscription Products Only"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:411
#: includes/switching/class-wc-subscriptions-switcher.php:421
msgctxt "when to prorate recurring fee when switching"
msgid "For Upgrades & Downgrades of All Subscription Products"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:416
#: includes/switching/class-wc-subscriptions-switcher.php:426
msgid "Prorate Sign up Fee"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:417
#: includes/switching/class-wc-subscriptions-switcher.php:427
msgid "When switching to a subscription with a sign up fee, you can require the customer pay only the gap between the existing subscription's sign up fee and the new subscription's sign up fee (if any)."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:425
#: includes/switching/class-wc-subscriptions-switcher.php:435
msgctxt "when to prorate signup fee when switching"
msgid "Never (do not charge a sign up fee)"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:426
#: includes/switching/class-wc-subscriptions-switcher.php:436
msgctxt "when to prorate signup fee when switching"
msgid "Never (charge the full sign up fee)"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:427
#: includes/switching/class-wc-subscriptions-switcher.php:437
msgctxt "when to prorate signup fee when switching"
msgid "Always"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:432
#: includes/switching/class-wc-subscriptions-switcher.php:442
msgid "Prorate Subscription Length"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:433
#: includes/switching/class-wc-subscriptions-switcher.php:443
msgid "When switching to a subscription with a length, you can take into account the payments already completed by the customer when determining how many payments the subscriber needs to make for the new subscription."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:448
#: includes/switching/class-wc-subscriptions-switcher.php:458
msgid "Switch Button Text"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:449
#: includes/switching/class-wc-subscriptions-switcher.php:459
msgid "Customise the text displayed on the button next to the subscription on the subscriber's account page. The default is \"Switch Subscription\", but you may wish to change this to \"Upgrade\" or \"Change Subscription\"."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:453
#: includes/switching/class-wc-subscriptions-switcher.php:555
#: includes/switching/class-wc-subscriptions-switcher.php:463
#: includes/switching/class-wc-subscriptions-switcher.php:565
msgid "Upgrade or Downgrade"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:492
#: includes/switching/class-wc-subscriptions-switcher.php:502
msgid "Allow Switching"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:499
#: includes/switching/class-wc-subscriptions-switcher.php:509
msgctxt "when to allow switching"
msgid "Between Subscription Variations"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:503
#: includes/switching/class-wc-subscriptions-switcher.php:513
msgctxt "when to allow switching"
msgid "Between Grouped Subscriptions"
msgstr ""
#. translators: %s: order number.
#: includes/switching/class-wc-subscriptions-switcher.php:1165
#: includes/switching/class-wc-subscriptions-switcher.php:1199
#, php-format
msgid "Switch order cancelled due to a new switch order being created #%s."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1396
#: includes/switching/class-wc-subscriptions-switcher.php:1478
#: includes/switching/class-wc-subscriptions-switcher.php:1434
#: includes/switching/class-wc-subscriptions-switcher.php:1518
msgid "The subscription may have been deleted."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1404
#: includes/switching/class-wc-subscriptions-switcher.php:1442
msgid "You can only switch to a subscription product."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1410
#: includes/switching/class-wc-subscriptions-switcher.php:1448
msgid "We can not find your old subscription item."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1432
#: includes/switching/class-wc-subscriptions-switcher.php:1470
msgid "You can not switch to the same subscription."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1483
#: includes/switching/class-wc-subscriptions-switcher.php:1523
msgid "You can not switch this subscription. It appears you do not own the subscription."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1550
#: includes/switching/class-wc-subscriptions-switcher.php:1590
msgid "There was an error locating the switch details."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1899
#: includes/switching/class-wc-subscriptions-switcher.php:1939
msgctxt "a switch type"
msgid "Downgrade"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1902
#: includes/switching/class-wc-subscriptions-switcher.php:1942
msgctxt "a switch type"
msgid "Upgrade"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:1905
#: includes/switching/class-wc-subscriptions-switcher.php:1945
msgctxt "a switch type"
msgid "Crossgrade"
msgstr ""
#. translators: %1: product subtotal, %2: HTML span tag, %3: direction (upgrade, downgrade, crossgrade), %4: closing HTML span tag
#: includes/switching/class-wc-subscriptions-switcher.php:1910
#: includes/switching/class-wc-subscriptions-switcher.php:1950
#, php-format
msgctxt "product subtotal string"
msgid "%1$s %2$s(%3$s)%4$s"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:2026
#: includes/switching/class-wc-subscriptions-switcher.php:2066
msgid "The original subscription item being switched cannot be found."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:2028
#: includes/switching/class-wc-subscriptions-switcher.php:2068
msgid "The item on the switch order cannot be found."
msgstr ""
#. translators: 1$: old item, 2$: new item when switching
#: includes/switching/class-wc-subscriptions-switcher.php:2039
#: includes/switching/class-wc-subscriptions-switcher.php:2079
#, php-format
msgctxt "used in order notes"
msgid "Customer switched from: %1$s to %2$s."
msgstr ""
#. translators: %s: new item name.
#: includes/switching/class-wc-subscriptions-switcher.php:2042
#: includes/switching/class-wc-subscriptions-switcher.php:2082
#, php-format
msgctxt "used in order notes"
msgid "Customer added %s."
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:2454
#: includes/switching/class-wc-subscriptions-switcher.php:2494
msgid "Switch Order"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:2469
#: includes/switching/class-wc-subscriptions-switcher.php:2509
msgid "Switched Subscription"
msgstr ""
#: includes/switching/class-wc-subscriptions-switcher.php:2656
#: includes/switching/class-wc-subscriptions-switcher.php:2696
msgctxt "add to cart button text while switching a subscription"
msgid "Switch subscription"
msgstr ""
@ -7507,7 +7525,7 @@ msgstr ""
#. translators: placeholder is trial period validation message if passed an invalid value (e.g. "Trial period can not exceed 4 weeks")
#: templates/admin/deprecated/html-variation-price.php:118
#: templates/admin/html-variation-price.php:83
#: templates/admin/html-variation-price.php:85
#, php-format
msgctxt "Trial period dropdown's description in pricing fields"
msgid "An optional period of time to wait before charging the first recurring payment. Any sign up fee will still be charged at the outset of the subscription. %s"
@ -7571,25 +7589,25 @@ msgstr[1] ""
msgid "To see further details about these errors, view the %1$s log file from the %2$sWooCommerce logs screen.%2$s"
msgstr ""
#: templates/admin/html-variation-price.php:35
#: templates/admin/html-variation-price.php:37
msgid "Billing interval:"
msgstr ""
#: templates/admin/html-variation-price.php:42
#: templates/admin/html-variation-price.php:44
msgid "Billing Period:"
msgstr ""
#: templates/admin/html-variation-price.php:56
#: templates/admin/html-variation-price.php:58
msgctxt "Subscription Length dropdown's description in pricing fields"
msgid "Automatically stop renewing the subscription after this length of time. This length is in addition to any free trial or amount of time provided before a synchronised first renewal date."
msgstr ""
#: templates/admin/html-variation-price.php:75
#: templates/admin/html-variation-price.php:77
msgctxt "example price"
msgid "e.g."
msgstr ""
#: templates/admin/html-variation-price.php:90
#: templates/admin/html-variation-price.php:92
msgid "Subscription trial period:"
msgstr ""
@ -8660,7 +8678,7 @@ msgstr ""
msgid "Clear"
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: build/admin.js:1
#: client/components/announcement/index.js:112
#: client/components/announcement/index.js:163
#: client/components/announcement/__tests__/index.test.js:118
@ -8668,7 +8686,7 @@ msgstr ""
msgid "Close"
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: build/admin.js:1
#: client/components/announcement/index.js:131
#: client/components/announcement/__tests__/index.test.js:70
#: client/components/announcement/__tests__/index.test.js:95
@ -8677,52 +8695,69 @@ msgstr ""
msgid "Step image"
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: build/admin.js:1
#: client/components/announcement/index.js:175
#: client/components/announcement/__tests__/index.test.js:115
msgid "Done"
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: client/gifting/data/welcome-announcement.js:8
msgid "Gifting is now part of WooCommerce Subscriptions"
#: build/admin.js:1
#: client/downloads/data/welcome-announcement.js:7
msgid "WooCommerce Subscription Downloads is now part of WooCommerce Subscriptions"
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: client/gifting/data/welcome-announcement.js:12
msgid "Introducing subscription gifting"
#: build/admin.js:1
#: client/downloads/data/welcome-announcement.js:12
msgid "No separate extension required! WooCommerce Subscriptions now allows you to include simple and variable products with your subscriptions. And to make it easier, you can now configure which simple and variable products to include when creating a subscription product."
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: client/gifting/data/welcome-announcement.js:15
msgid "No separate extension needed! The built-in gifting feature is fully compatible with product, cart and checkout blocks, plus you can now choose which subscription products can be gifted."
#: build/admin.js:1
#: client/downloads/data/welcome-announcement.js:18
msgid "The WooCommerce Subscription Downloads extension can now be disabled via Plugins."
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: client/gifting/data/welcome-announcement.js:21
msgid "The Gifting for WooCommerce Subscriptions extension can now be disabled via Plugins."
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: client/gifting/data/welcome-announcement.js:26
msgid "Let your shoppers purchase subscriptions as gifts using the new built-in gifting feature in WooCommerce Subscriptions. It works seamlessly with product, cart and checkout blocks, and can be enabled storewide or managed per product."
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: build/admin.js:1
#: client/downloads/data/welcome-announcement.js:24
#: client/gifting/data/welcome-announcement.js:33
msgid "Go to plugins"
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: client/gifting/data/welcome-announcement.js:34
msgid "Set up gifting"
msgstr ""
#: build/gifting-welcome-announcement.js:1
#: build/admin.js:1
#: client/downloads/data/welcome-announcement.js:35
#: client/gifting/data/welcome-announcement.js:42
msgid "Maybe later"
msgstr ""
#: build/admin.js:1
#: client/gifting/data/welcome-announcement.js:8
msgid "Gifting is now part of WooCommerce Subscriptions"
msgstr ""
#: build/admin.js:1
#: client/gifting/data/welcome-announcement.js:12
msgid "Introducing subscription gifting"
msgstr ""
#: build/admin.js:1
#: client/gifting/data/welcome-announcement.js:15
msgid "No separate extension needed! The built-in gifting feature is fully compatible with product, cart and checkout blocks, plus you can now choose which subscription products can be gifted."
msgstr ""
#: build/admin.js:1
#: client/gifting/data/welcome-announcement.js:21
msgid "The Gifting for WooCommerce Subscriptions extension can now be disabled via Plugins."
msgstr ""
#: build/admin.js:1
#: client/gifting/data/welcome-announcement.js:26
msgid "Let your shoppers purchase subscriptions as gifts using the new built-in gifting feature in WooCommerce Subscriptions. It works seamlessly with product, cart and checkout blocks, and can be enabled storewide or managed per product."
msgstr ""
#: build/admin.js:1
#: client/gifting/data/welcome-announcement.js:34
msgid "Set up gifting"
msgstr ""
#: build/index.js:1
#: client/core/utils/index.js:8
msgctxt "Used in recurring totals section in Cart. 2+ will need plural, 1 will need singular."

3717
phpstan.neon Normal file

File diff suppressed because it is too large Load Diff

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

@ -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.2.1',
'version' => 'dev-release/8.2.1',
'reference' => '8bb99631f731722b758f4702dc319ae43043ed67',
'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.2.1',
'version' => 'dev-release/8.2.1',
'reference' => '8bb99631f731722b758f4702dc319ae43043ed67',
'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,7 +5,7 @@
* Description: Sell products and services with recurring payments in your WooCommerce Store.
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* Version: 8.2.1
* Version: 8.3.0
* Requires Plugins: woocommerce
*
* WC requires at least: 10.3.0
@ -84,7 +84,7 @@ class WC_Subscriptions {
public static $plugin_file = __FILE__;
/** @var string */
public static $version = '8.2.1'; // WRCS: DEFINED_VERSION.
public static $version = '8.3.0'; // WRCS: DEFINED_VERSION.
/** @var string */
public static $wc_minimum_supported_version = '7.7';