Compare commits

...

9 Commits
v7.8.0 ... main

Author SHA1 Message Date
WooCommerce 56e03c0544 Updates to 8.3.0 2026-01-08 10:21:29 +00:00
WooCommerce bfe7fb609d Updates to 8.2.1 2025-12-24 10:19:37 +00:00
WooCommerce c7e30d354e Updates to 8.2.0 2025-12-11 10:21:12 +00:00
WooCommerce dda5b19e32 Updates to 8.1.0 2025-11-14 10:19:28 +00:00
WooCommerce c8ec766bf2 Updates to 8.0.0 2025-10-16 10:18:57 +00:00
WooCommerce 7b355505bf Updates to 8.0.0 2025-10-15 10:18:29 +00:00
WooCommerce 708a1fa4a4 Updates to 7.9.0 2025-09-18 10:16:37 +00:00
WooCommerce 7d081fb691 Updates to 7.8.2 2025-09-02 10:17:52 +00:00
WooCommerce 9932a571ff Updates to 7.8.1 2025-08-26 10:18:06 +00:00
217 changed files with 12326 additions and 8954 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

@ -28,3 +28,40 @@
border-color: var(--wc-red, #cc1818);
color: var(--wc-red, #cc1818);
}
.wcsg_add_recipient_fields_container label {
display: inline-block;
margin-bottom: 20px;
}
.woocommerce-cart-form__cart-item .wcsg_add_recipient_fields_container label {
margin-bottom: 4px;
}
.wcsg_add_recipient_fields_container .wcsg_add_recipient_fields .woocommerce_subscriptions_gifting_recipient_email {
padding: 0;
margin-bottom: 20px;
}
.wcsg_add_recipient_fields_container .wcsg_add_recipient_fields.hidden {
display: none;
}
.wcsg_add_recipient_fields_container .recipient_email:focus {
outline-offset: -2px;
}
#woocommerce_subscriptions_gifting_field {
display: flex;
flex-wrap: wrap;
align-items: center;
max-width: 100%;
}
#woocommerce_subscriptions_gifting_field div {
display: inline-flex;
align-items: center;
max-width: 100%;
word-break: break-all;
word-wrap: break-word;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,8 @@ jQuery( document ).ready( function ( $ ) {
.find( '.recipient_email' )
.trigger( 'focus' );
}
} );
} )
.removeClass( 'hidden' );
const shipToDifferentAddressCheckbox = $( document ).find(
'#ship-to-different-address-checkbox'
@ -31,12 +32,14 @@ jQuery( document ).ready( function ( $ ) {
$( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.wcsg_add_recipient_fields' )
.slideUp( 250 );
.slideUp( 250 )
.addClass( 'hidden' );
const recipientEmailElement = $( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.recipient_email' );
recipientEmailElement.val( '' );
hideValidationErrorForEmailField( recipientEmailElement );
setShippingAddressNoticeVisibility( true );
if ( $( 'form.checkout' ).length !== 0 ) {
@ -156,8 +159,8 @@ jQuery( document ).ready( function ( $ ) {
'.woocommerce-cart-form :input[type="submit"][name="update_cart"]'
);
if ( $updateCartButton.length ) {
$updateCartButton.prop( 'disabled', ! allValid );
if ( $updateCartButton.length && ! allValid ) {
$updateCartButton.prop( 'disabled', true );
}
return allValid;

View File

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

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

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

1
build/admin.js Normal file

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
.wcs-recurring-totals-panel{padding:1em 0 0;position:relative}.wcs-recurring-totals-panel:after{border-style:solid;border-width:1px 0;bottom:0;content:"";display:block;right:0;opacity:.3;pointer-events:none;position:absolute;left:0;top:0}.wcs-recurring-totals-panel+.wcs-recurring-totals-panel:after{border-top-width:0}.wcs-recurring-totals-panel .wc-block-components-panel .wc-block-components-totals-item{padding-right:0;padding-left:0}.wcs-recurring-totals-panel .wc-block-components-totals-item__label:first-letter{text-transform:capitalize}.wcs-recurring-totals-panel .wcs-recurring-totals-panel__title .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals-panel__title{margin:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__button,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:focus,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:hover{font-size:.875em}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:first-child{margin-top:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:last-child{margin-bottom:0}.wcs-recurring-totals-panel__details .wcs-recurring-totals-panel__details-total .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals__subscription-length{float:left}
.wcs-recurring-totals-panel{padding:1em 0 0;position:relative}.wcs-recurring-totals-panel:after{border-style:solid;border-width:1px 0;bottom:0;content:"";display:block;right:0;opacity:.3;pointer-events:none;position:absolute;left:0;top:0}.wcs-recurring-totals-panel+.wcs-recurring-totals-panel:after{border-top-width:0}.wcs-recurring-totals-panel .wc-block-components-panel .wc-block-components-totals-item{padding-right:0;padding-left:0}.wcs-recurring-totals-panel .wc-block-components-totals-item__label:first-letter{text-transform:capitalize}.wcs-recurring-totals-panel .wcs-recurring-totals-panel__title .wc-block-components-totals-item__label{font-weight:500}.wcs-recurring-totals-panel__title{margin:0}.wc-block-components-main .wcs-recurring-totals-panel__details{padding:0 16px}.wcs-recurring-totals-panel__details .wc-block-components-panel__button,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:focus,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:hover{font-size:.875em}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:first-child{margin-top:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:last-child{margin-bottom:0}.wcs-recurring-totals-panel__details .wcs-recurring-totals-panel__details-total .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals__subscription-length{float:left}
.wc-block-components-local-pickup-rates-control .wc-block-components-local-pickup-select:not(:last-child){margin-bottom:16px}
.wcsg_add_recipient_fields_container label{display:inline-block;margin-bottom:20px}.wcsg_add_recipient_fields_container .wcsg_add_recipient_fields .woocommerce_subscriptions_gifting_recipient_email{margin-bottom:20px;padding:0}.wcsg_add_recipient_fields_container .recipient_email:focus{outline-offset:-2px}.wcsg-gifting-to-container-editing{display:flex;gap:5px;margin-top:12px}.wcsg-gifting-to-container-editing .wc-block-components-text-input{flex-grow:1;margin-top:0}.wcsg-gifting-to-container-editing .wp-element-button.gifting-update-button:not(.is-link){min-height:unset;padding:0 var(--xs,20px)}.wcsg-gifting-to-container-editing .components-base-control__field{margin-bottom:0}.wcsg-gifting-to-container-editing .has-error .components-text-control__input{color:var(--wc-red,#cc1818)}.wcsg-gifting-to-container-view{display:flex;gap:5px}.wcsg-gifting-to-container-view .components-button.is-link{color:var(--wp--preset--color--contrast);font-size:medium}.wcsg-block-recipient-container .components-checkbox-control__label{font-size:medium}.wc-block-cart .wc-block-components-product-details__gifting-to,.wc-block-cart .wc-block-components-product-details__gifting-to-hidden,.wc-block-cart .wc-block-components-product-details__item-key,.wc-block-checkout .wc-block-components-product-details__gifting-to-hidden,.wc-block-checkout .wc-block-components-product-details__item-key{display:none}

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wc-blocks-checkout', 'wc-price-format', 'wc-settings', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'a84e83df20234c984f3e');
<?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');

View File

@ -1,3 +1,3 @@
.wcs-recurring-totals-panel{padding:1em 0 0;position:relative}.wcs-recurring-totals-panel:after{border-style:solid;border-width:1px 0;bottom:0;content:"";display:block;left:0;opacity:.3;pointer-events:none;position:absolute;right:0;top:0}.wcs-recurring-totals-panel+.wcs-recurring-totals-panel:after{border-top-width:0}.wcs-recurring-totals-panel .wc-block-components-panel .wc-block-components-totals-item{padding-left:0;padding-right:0}.wcs-recurring-totals-panel .wc-block-components-totals-item__label:first-letter{text-transform:capitalize}.wcs-recurring-totals-panel .wcs-recurring-totals-panel__title .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals-panel__title{margin:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__button,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:focus,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:hover{font-size:.875em}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:first-child{margin-top:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:last-child{margin-bottom:0}.wcs-recurring-totals-panel__details .wcs-recurring-totals-panel__details-total .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals__subscription-length{float:right}
.wcs-recurring-totals-panel{padding:1em 0 0;position:relative}.wcs-recurring-totals-panel:after{border-style:solid;border-width:1px 0;bottom:0;content:"";display:block;left:0;opacity:.3;pointer-events:none;position:absolute;right:0;top:0}.wcs-recurring-totals-panel+.wcs-recurring-totals-panel:after{border-top-width:0}.wcs-recurring-totals-panel .wc-block-components-panel .wc-block-components-totals-item{padding-left:0;padding-right:0}.wcs-recurring-totals-panel .wc-block-components-totals-item__label:first-letter{text-transform:capitalize}.wcs-recurring-totals-panel .wcs-recurring-totals-panel__title .wc-block-components-totals-item__label{font-weight:500}.wcs-recurring-totals-panel__title{margin:0}.wc-block-components-main .wcs-recurring-totals-panel__details{padding:0 16px}.wcs-recurring-totals-panel__details .wc-block-components-panel__button,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:focus,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:hover{font-size:.875em}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:first-child{margin-top:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:last-child{margin-bottom:0}.wcs-recurring-totals-panel__details .wcs-recurring-totals-panel__details-total .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals__subscription-length{float:right}
.wc-block-components-local-pickup-rates-control .wc-block-components-local-pickup-select:not(:last-child){margin-bottom:16px}
.wcsg_add_recipient_fields_container label{display:inline-block;margin-bottom:20px}.wcsg_add_recipient_fields_container .wcsg_add_recipient_fields .woocommerce_subscriptions_gifting_recipient_email{margin-bottom:20px;padding:0}.wcsg_add_recipient_fields_container .recipient_email:focus{outline-offset:-2px}.wcsg-gifting-to-container-editing{display:flex;gap:5px;margin-top:12px}.wcsg-gifting-to-container-editing .wc-block-components-text-input{flex-grow:1;margin-top:0}.wcsg-gifting-to-container-editing .wp-element-button.gifting-update-button:not(.is-link){min-height:unset;padding:0 var(--xs,20px)}.wcsg-gifting-to-container-editing .components-base-control__field{margin-bottom:0}.wcsg-gifting-to-container-editing .has-error .components-text-control__input{color:var(--wc-red,#cc1818)}.wcsg-gifting-to-container-view{display:flex;gap:5px}.wcsg-gifting-to-container-view .components-button.is-link{color:var(--wp--preset--color--contrast);font-size:medium}.wcsg-block-recipient-container .components-checkbox-control__label{font-size:medium}.wc-block-cart .wc-block-components-product-details__gifting-to,.wc-block-cart .wc-block-components-product-details__gifting-to-hidden,.wc-block-cart .wc-block-components-product-details__item-key,.wc-block-checkout .wc-block-components-product-details__gifting-to-hidden,.wc-block-checkout .wc-block-components-product-details__item-key{display:none}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.woocommerce-subscriptions-announcement__container{border-radius:2px;bottom:44px;cursor:default;display:inline;position:fixed;left:16px;z-index:9999}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step{box-shadow:0 2px 3px 0 rgba(0,0,0,.05),0 4px 5px 0 rgba(0,0,0,.04),0 4px 5px 0 rgba(0,0,0,.03),0 16px 16px 0 rgba(0,0,0,.02)}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step .components-elevation{display:none}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header{position:absolute}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image{background-color:#f2edff;border-radius:0;height:140px;padding:18px}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image img{margin:0 auto;max-width:100%;width:120px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__body{padding-top:8px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step-navigation{justify-content:end}
.woocommerce-subscriptions-announcement__container{border-radius:2px;bottom:44px;cursor:default;display:inline;position:fixed;left:16px;z-index:9999}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step{box-shadow:0 2px 3px 0 rgba(0,0,0,.05),0 4px 5px 0 rgba(0,0,0,.04),0 4px 5px 0 rgba(0,0,0,.03),0 16px 16px 0 rgba(0,0,0,.02)}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step .components-elevation{display:none}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header{position:absolute}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image{background-color:#f2edff;border-radius:0;height:140px;padding:18px}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image img{margin:0 auto;max-width:100%;width:120px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__body{padding-top:8px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step-navigation{justify-content:end!important}

View File

@ -1 +1 @@
.woocommerce-subscriptions-announcement__container{border-radius:2px;bottom:44px;cursor:default;display:inline;position:fixed;right:16px;z-index:9999}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step{box-shadow:0 2px 3px 0 rgba(0,0,0,.05),0 4px 5px 0 rgba(0,0,0,.04),0 4px 5px 0 rgba(0,0,0,.03),0 16px 16px 0 rgba(0,0,0,.02)}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step .components-elevation{display:none}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header{position:absolute}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image{background-color:#f2edff;border-radius:0;height:140px;padding:18px}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image img{margin:0 auto;max-width:100%;width:120px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__body{padding-top:8px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step-navigation{justify-content:end}
.woocommerce-subscriptions-announcement__container{border-radius:2px;bottom:44px;cursor:default;display:inline;position:fixed;right:16px;z-index:9999}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step{box-shadow:0 2px 3px 0 rgba(0,0,0,.05),0 4px 5px 0 rgba(0,0,0,.04),0 4px 5px 0 rgba(0,0,0,.03),0 16px 16px 0 rgba(0,0,0,.02)}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step .components-elevation{display:none}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header{position:absolute}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image{background-color:#f2edff;border-radius:0;height:140px;padding:18px}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__header-image img{margin:0 auto;max-width:100%;width:120px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step__body{padding-top:8px!important}.woocommerce-subscriptions-announcement .woocommerce-tour-kit-step-navigation{justify-content:end!important}

View File

@ -0,0 +1 @@
.wcsg-gifting-to-container-editing{display:flex;gap:5px;margin-top:10px;min-width:210px}.wcsg-gifting-to-container-editing .wc-block-components-text-input{flex-grow:1;margin-top:0}.wcsg-gifting-to-container-editing .wp-element-button.gifting-update-button:not(.is-link){min-height:unset;padding:0 var(--xs,20px)}.wcsg-gifting-to-container-editing .components-base-control__field{margin-bottom:0}.wcsg-gifting-to-container-editing .wc-block-components-text-input input:-webkit-autofill{padding-top:.5em}.wcsg-gifting-to-container-editing .has-error .components-text-control__input{color:var(--wc-red,#cc1818)}.wp-block-woocommerce-mini-cart-contents .wcsg-gifting-to-container-editing{flex-direction:column}.wp-block-woocommerce-mini-cart-contents .gifting-update-button{height:40px}.wcsg-gifting-to-container-view{align-items:center;display:flex;flex-wrap:wrap;gap:5px;max-width:100%}.wcsg-gifting-to-container-view span{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word;gap:5px}.wcsg-gifting-to-container-view .components-button.is-link{color:var(--wp--preset--color--contrast);flex-shrink:0;font-size:medium}.wc-block-checkout,.wc-block-components-product-details__gifting-to{align-items:center;display:flex;flex-wrap:wrap;max-width:100%}.wc-block-checkout .wc-block-components-product-details__name:after,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__name:after{content:" "}.wc-block-checkout .wc-block-components-product-details__value,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__value{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word}.wcsg-block-recipient-container{font-size:var(--wp--preset--font-size--small,14px);line-height:20px}.wcsg-block-recipient-container .components-checkbox-control__label{font-size:var(--wp--preset--font-size--small,14px);margin-bottom:0}.wc-block-cart .wc-block-components-product-details__gifting-to,.wc-block-cart .wc-block-components-product-details__gifting-to-hidden,.wc-block-cart .wc-block-components-product-details__item-key,.wc-block-checkout .wc-block-components-product-details__gifting-to-hidden,.wc-block-checkout .wc-block-components-product-details__item-key,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to-hidden,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__item-key{display:none}

View File

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

View File

@ -0,0 +1 @@
.wcsg-gifting-to-container-editing{display:flex;gap:5px;margin-top:10px;min-width:210px}.wcsg-gifting-to-container-editing .wc-block-components-text-input{flex-grow:1;margin-top:0}.wcsg-gifting-to-container-editing .wp-element-button.gifting-update-button:not(.is-link){min-height:unset;padding:0 var(--xs,20px)}.wcsg-gifting-to-container-editing .components-base-control__field{margin-bottom:0}.wcsg-gifting-to-container-editing .wc-block-components-text-input input:-webkit-autofill{padding-top:.5em}.wcsg-gifting-to-container-editing .has-error .components-text-control__input{color:var(--wc-red,#cc1818)}.wp-block-woocommerce-mini-cart-contents .wcsg-gifting-to-container-editing{flex-direction:column}.wp-block-woocommerce-mini-cart-contents .gifting-update-button{height:40px}.wcsg-gifting-to-container-view{align-items:center;display:flex;flex-wrap:wrap;gap:5px;max-width:100%}.wcsg-gifting-to-container-view span{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word;gap:5px}.wcsg-gifting-to-container-view .components-button.is-link{color:var(--wp--preset--color--contrast);flex-shrink:0;font-size:medium}.wc-block-checkout,.wc-block-components-product-details__gifting-to{align-items:center;display:flex;flex-wrap:wrap;max-width:100%}.wc-block-checkout .wc-block-components-product-details__name:after,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__name:after{content:" "}.wc-block-checkout .wc-block-components-product-details__value,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__value{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word}.wcsg-block-recipient-container{font-size:var(--wp--preset--font-size--small,14px);line-height:20px}.wcsg-block-recipient-container .components-checkbox-control__label{font-size:var(--wp--preset--font-size--small,14px);margin-bottom:0}.wc-block-cart .wc-block-components-product-details__gifting-to,.wc-block-cart .wc-block-components-product-details__gifting-to-hidden,.wc-block-cart .wc-block-components-product-details__item-key,.wc-block-checkout .wc-block-components-product-details__gifting-to-hidden,.wc-block-checkout .wc-block-components-product-details__item-key,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to-hidden,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__item-key{display:none}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,95 @@
*** WooCommerce Subscriptions Changelog ***
2026-01-06 - version 8.3.0
* Add: Support for ajax add-to-cart flows from the single product page, when upgrading or downgrading subscriptions.
* Add: Enhanced functionality from the WooCommerce Subscription Downloads plugin has now been integrated directly into WooCommerce Subscriptions.
* Fix: Ensured the subject line for the "Customer Renewal Invoice" email correctly updates.
2025-12-22 - version 8.2.1
* Fix: Subscription pricing strings now correctly display singular and plural billing intervals like "$10 / month" and "$10 every 3 months" in cart and checkout totals.
* Fix: Display all recurring coupon names in the totals section of classic cart and checkout when multiple recurring coupons are applied.
* Fix: Payment method selector options on Subscriptions list page are now properly displaying the payment method name for gateways providing multiple payment methods.
* Fix: PayPal Standard plugin IPN renewal orders not being created for failed payment retries.
* Fix: Prevent fatal errors on shop pages when special characters like % are used in a product name.
* Fix: Prevent an email validation error from briefly and inadvertently flashing on the screen when the "this is a gift" checkbox is used.
2025-12-10 - version 8.2.0
* Fix: Various accessibility issues.
* Added button roles.
* Added labels to remove product links.
* Adding missing header labels on related orders table.
* Prevented various hidden elements from being exposed to screen readers.
* Fixed focus problems for the renewal dialog.
* Fixed focus when closing modals.
* Fixed toggle state change using spacebar.
* Fixed label for sign up now button.
* Fixed toggle aria-label text to reflect state.
* Added aria-haspopup attribute to buttons that open modals.
* Fixed link for view order aria-label.
* Fixed renew now button text and aria-label.
* Added aria-modal and role attributes to modals and dialogs.
* Fix: Resubscribe button now appears correctly for cancelled limited subscriptions.
* Fix: Missing translation for pricing string on languages without plural form.
* Fix: Customized subscriptions links in WooCommerce emails now work correctly.
* Fix: Possible fatal errors when switching grouped subscriptions.
* Fix: Prevent unnecessary log entries related to gifted subscriptions.
* Fix: Make it easier for translation plugins to respect individual customer language preferences when sending subscription emails.
* Fix: Prevent recurring local pickup options from displaying for virtual subscriptions when the cart contains physical products.
* Fix: Prevent shipping summary from displaying on Blocks Checkout for virtual subscriptions.
* Fix: Item price in blocks checkout is correctly displayed without "due today" words for items with zero sign up fee.
2025-11-13 - version 8.1.0
* Fix: Prevent a fatal error that can occur when previewing emails in WooCommerce email settings.
* Fix: Prevent technical subscription-specific discount types from appearing in the coupon edit UI.
* Fix: Add admin notice and debug tool to recreate the payment retry database table when it's missing.
* Fix: Prevent resubscribe and reactivate buttons from showing for subscriptions containing disabled, deleted, draft, or limited products.
* Fix: Prevent error that blocks the subscriptions list from loading when gifted subscriptions are purchased with certain payment methods.
* Fix: Ensure gift recipient email address is displayed in the order confirmation email sent to the purchaser.
* Fix: Prevent pickup location options from displaying incorrectly alongside shipping methods on the Blocks Checkout page.
* Fix: Fix invalid HTML tags that cause fatal errors when using themes with WordPress Interactivity API.
* Fix: Add password reset links to the new account email template for gifted subscription recipients.
* Tweak: Improve the gift recipient email input field on Blocks Cart and Mini Cart to match the style of other checkout fields.
* Dev: Improve housekeeping of output buffers when handling requests to change subscription payment methods.
* Dev: Improve type safety in relation to the `wcs_get_subscription()` function.
2025-10-15 - version 8.0.0
* Add: Blocks Checkout now displays a single shipping method selection for both initial and recurring carts, similar to Classic Checkout. This can be disabled using the `wcs_cart_totals_shipping_html_price_only` filter.
* Fix: Resubscribing to a limited subscription from the product page will now reactivate the existing subscription.
* Fix: "New initial order - Recipient" email template preview no longer causes critical error.
* Fix: Shipping value on blocks checkout no longer shows as "Free" when no shipping method is available.
* Dev: Improved data telemetry which is sent to WooCommerce:
* WooCommerce Subscriptions setting values
* Order totals of subscription-related orders by order type and payment gateway, aggregated by month
* Number of subscription-related orders by order type and payment gateway, aggregated by month
* Number of products in parent subscription orders, aggregated by month
* Number of non-zero value parent subscription orders, aggregated by month
* Number of subscription products by billing interval
* Number of giftable subscription products
* Number of subscriptions by payment method, billing period, and renewal type
* Number of gifted subscriptions
* Number of subscribers
* Purchase event for subscription products
* Exclude orders with a "processing" status from the "subscription_orders" telemetry data.
2025-09-16 - version 7.9.0
* Add: REST API endpoint to retrieve subscriptions associated with a specific order ID.
* Fix: Keep subscription with successful renewals active when parent order fails.
* Fix: Error when renewing subscriptions with downloadable gifts.
* Fix: Improve performance by loading assets only when necessary.
* Fix: "Update cart" button being always enabled.
* Fix: Gifting recipient email layout issues when using a long e-mail address.
* Fix: Gifting checkbox not showing up on blocks cart when using WooCommerce 10.2+.
* Fix: Deleting a gifted subscription recipient will not delete their subscriptions, they will be moved to the subscription purchaser instead.
* Dev: The `wcs_get_subscriptions()` and `wcs_get_subscriptions_for_order()` functions are now stricter, and only return instances of `WC_Subscription`.
2025-09-02 - version 7.8.2
* Fix: Error when updating/renewing subscriptions containing a pseudo coupon.
* Fix: Fix gifting recipient notice being shown for non gifting orders.
2025-08-25 - version 7.8.1
* Fix: Fix checkout button default label.
* Fix: Fix error when WP CLI is enabled in some environments.
2025-08-19 - version 7.8.0
* Add: Native support for subscriptions gifting. Gifting for WooCommerce Subscriptions extension is no longer required.
* Add: Enable subscriptions gifting storewide and per product.
@ -14,7 +104,7 @@
2025-07-09 - version 7.7.0
* Fix: Restores normal behavior for the report caching updates scheduled action, which was failing due to a bad filepath.
* Fix: Fix error when placing an order with a valid card after using a declined one.
* Fix: Fix order renewal errors when using some plugins.
* Fix: Fix order renewal errors when using some plugins.
* Fix: Prevent fatal errors when loading or deleting subscriptions with corrupted date values stored in a database.
* Fix: Prevent fatal errors when trying to save invalid date values and display better error messages instead.
* Fix: Fix broken blocks and javascript translations.

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

@ -36,6 +36,8 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
* -- Subscription specific --
* GET /subscriptions/status
* GET /subscriptions/<subscription_id>/orders
* GET /orders/<order_id>/subscriptions
* POST /orders/<order_id>/subscriptions
*
* @since 3.1.0
*/
@ -61,15 +63,25 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, "/orders/(?P<id>[\d]+)/{$this->rest_base}", array(
register_rest_route(
$this->namespace,
"/orders/(?P<id>[\d]+)/{$this->rest_base}",
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_subscriptions_from_order' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_order_subscriptions' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_subscriptions_from_order' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
@ -206,6 +218,76 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
return apply_filters( 'wcs_rest_subscription_orders_response', $response, $request );
}
/**
* Gets the /orders/[id]/subscriptions response.
*
* @since 7.9.0
*
* @param WP_REST_Request $request The request object.
* @return WP_Error|WP_REST_Response $response The response or an error if one occurs.
*/
public function get_order_subscriptions( $request ) {
$order_id = absint( $request['id'] );
if ( empty( $order_id ) ) {
return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
return new WP_Error(
'woocommerce_rest_invalid_order_id',
// translators: %d is the order ID.
sprintf( __( 'Failed to load order object with the ID %d.', 'woocommerce-subscriptions' ), $order_id ),
array( 'status' => 404 )
);
}
// Build arguments for wcs_get_subscriptions_for_order()x
$args = array(
'order_type' => 'any', // Return all subscriptions for the order.
);
// Set arguments from request.
if ( ! empty( $request['orderby'] ) ) {
$args['orderby'] = $request['orderby'];
}
if ( ! empty( $request['order'] ) ) {
$args['order'] = $request['order'];
}
// Map standard request parameters to wcs_get_subscriptions_for_order() arguments.
if ( ! empty( $request['customer'] ) ) {
$args['customer_id'] = $request['customer'];
}
if ( ! empty( $request['status'] ) ) {
$args['subscription_status'] = $request['status'];
}
$subscriptions = wcs_get_subscriptions_for_order( $order, $args );
$response_data = array();
foreach ( $subscriptions as $subscription ) {
if ( is_a( $subscription, 'WC_Subscription' ) && ! wc_rest_check_post_permissions( 'shop_subscription', 'read', $subscription->get_id() ) ) {
continue;
}
$response = $this->prepare_object_for_response( $subscription, $request );
$response_data[] = $this->prepare_response_for_collection( $response );
}
$response = rest_ensure_response( $response_data );
// Pagination is not fully supported, so we manually set the total.
$response->header( 'X-WP-Total', count( $response_data ) );
$response->header( 'X-WP-TotalPages', 1 );
return $response;
}
/**
* Overrides WC_REST_Orders_Controller::get_order_statuses() so that subscription statuses are
* validated correctly.

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

@ -83,6 +83,10 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
if ( ! empty( $post->post_type ) && ! empty( $post->ID ) && 'shop_subscription' == $post->post_type ) {
$subscription = wcs_get_subscription( $post->ID );
if ( ! $subscription ) {
return $response;
}
$response->data['billing_period'] = $subscription->get_billing_period();
$response->data['billing_interval'] = $subscription->get_billing_interval();
@ -292,8 +296,13 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
return new WP_Error( 'woocommerce_rest_invalid_shop_subscription_id', __( 'Invalid subscription id.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
}
$this->post_type = 'shop_order';
$subscription = wcs_get_subscription( $id );
$this->post_type = 'shop_order';
$subscription = wcs_get_subscription( $id );
if ( ! $subscription ) {
return new WP_Error( 'woocommerce_rest_invalid_shop_subscription_id', __( 'Invalid subscription id.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
}
$subscription_orders = $subscription->get_related_orders();
$orders = array();
@ -371,6 +380,10 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
// In particular, the update_post_meta() called while _stripe_card_id is updated to _stripe_source_id
$subscription = wcs_get_subscription( $subscription->get_id() );
if ( ! $subscription ) {
throw new WC_REST_Exception( 'woocommerce_rest_payment_update_failed', __( 'Subscription payment method could not be set updated due to technical issues.', 'woocommerce-subscriptions' ), 500 );
}
if ( isset( $payment_method_meta[ $payment_method ] ) ) {
$payment_method_meta = $payment_method_meta[ $payment_method ];

View File

@ -702,12 +702,16 @@ class WC_REST_Subscriptions_V2_Controller extends WC_REST_Orders_V2_Controller {
$subscription->add_item( $item );
}
/*
* Fetch a fresh instance of the subscription because the current instance has an empty line item cache generated before we had copied the line items.
* Fetching a new instance will ensure the line items are used when calculating totals.
*/
$subscription = wcs_get_subscription( $subscription->get_id() );
if ( ! $subscription ) {
throw new Exception( __( 'There was a problem completing this request. The subscription may have been deleted by another process.', 'woocommerce-subscriptions' ) );
}
$subscription->calculate_totals();
/**
@ -719,7 +723,13 @@ class WC_REST_Subscriptions_V2_Controller extends WC_REST_Orders_V2_Controller {
*/
do_action( "woocommerce_rest_insert_{$this->post_type}_object", $subscription, $request, true );
$response = $this->prepare_object_for_response( wcs_get_subscription( $subscription->get_id() ), $request );
$fresh_subscription = wcs_get_subscription( $subscription->get_id() );
if ( ! $fresh_subscription ) {
throw new Exception( __( 'There was a problem completing this request. The subscription may have been deleted by another process.', 'woocommerce-subscriptions' ) );
}
$response = $this->prepare_object_for_response( $fresh_subscription, $request );
$subscriptions[] = $this->prepare_response_for_collection( $response );
}
} catch ( Exception $e ) {

View File

@ -117,6 +117,13 @@ class WC_Subscriptions_Dependency_Manager {
$this->wc_version_cached = true;
// Try to get version from transient first
$this->wc_active_version = get_transient( 'wcs_woocommerce_active_version' );
if ( false !== $this->wc_active_version ) {
return $this->wc_active_version;
}
// Load plugin.php if it's not already loaded.
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
@ -139,9 +146,15 @@ class WC_Subscriptions_Dependency_Manager {
if ( $is_woocommerce && is_plugin_active( $plugin_slug ) ) {
$this->wc_active_version = $plugin_data['Version'];
break; // Found it, no need to continue looping
}
}
// Cache the result in a transient for 1 hour
if ( ! empty( $this->wc_active_version ) ) {
set_transient( 'wcs_woocommerce_active_version', $this->wc_active_version, HOUR_IN_SECONDS );
}
return $this->wc_active_version;
}

View File

@ -8,6 +8,8 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce_Subscriptions\Internal\Telemetry\Events as WC_Tracks_Events;
/**
* @method static WC_Subscriptions_Plugin instance()
*/
@ -32,13 +34,19 @@ 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();
if ( defined( 'WP_CLI' ) && WP_CLI ) {
new WC_Subscriptions_CLI();
}
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_show_welcome_message' ) );
add_action( 'plugins_loaded', array( $this, 'init_gifting' ), 20 );
add_action( 'plugins_loaded', array( $this, 'init_gifting' ) );
add_action( 'plugins_loaded', array( $this, 'init_downloads' ) );
add_action( 'admin_notices', array( WC_Subscription_Downloads_Settings::class, 'add_notice_about_bundled_feature' ) );
}
/**
@ -258,6 +266,7 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
require_once $gifting_includes . 'wcsg-compatibility-functions.php';
WCSG_Admin_Order::init();
WCSG_Product::init();
WCSG_Cart::init();
WCSG_Checkout::init();
@ -280,6 +289,34 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
WCS_Gifting::init();
}
/**
* Attempts to initialize additional downloads functionality.
*
* This functionality makes it possible to link downloadable products with a subscription
* product. Purchasers of the subscription product then automatically get access to the files associated with downloadable product.
*
* Previously, this functionality existed as a standalone plugin (WooCommerce Subscription Downnloads) and so,
* before initializing, we try to determine if the standalone plugin is active and has already loaded (if it is
* active, we do not proceed).
*/
public function init_downloads() {
if (
$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;
}
WC_Subscription_Downloads::setup();
}
/**
* Tries to determine if the specified plugin is being activated.
*
@ -318,10 +355,14 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
// Note that flags such as `--no-color` are filtered out of this array.
$args = WP_CLI::get_runner()->arguments;
if ( ! is_countable( $args ) ) {
return false;
}
if (
count( $args ) < 3
|| $args[0] !== 'plugin'
|| $args[1] !== 'activate'
( count( $args ) < 3
|| 'plugin' !== $args[0]
|| 'activate' !== $args[1] )
) {
return false;
}

View File

@ -8,6 +8,8 @@
* @since 2.0
*/
use Automattic\Jetpack\Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@ -153,7 +155,7 @@ class WCS_API {
* @return boolean
*/
protected static function is_orders_api_request() {
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST || empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
if ( ! Constants::is_true( 'REST_REQUEST' ) || empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
return false;
}

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

@ -30,14 +30,14 @@ class WCS_Call_To_Action_Button_Text_Manager {
public static function add_settings( $settings ) {
$button_text_settings = array(
array(
'name' => __( 'Button Text', 'woocommerce-subscriptions' ),
'name' => __( 'Button text', 'woocommerce-subscriptions' ),
'type' => 'title',
'desc' => '',
'id' => WC_Subscriptions_Admin::$option_prefix . '_button_text',
),
array(
'name' => __( 'Add to Cart Button Text', 'woocommerce-subscriptions' ),
'desc' => __( 'A product displays a button with the text "Add to cart". By default, a subscription changes this to "Sign up now". You can customise the button text for subscriptions here.', 'woocommerce-subscriptions' ),
'desc' => __( 'A product displays a button with the text "Add to cart". You can customise the button text for subscriptions here.', 'woocommerce-subscriptions' ),
'tip' => '',
'id' => WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text',
'css' => 'min-width:150px;',
@ -48,14 +48,14 @@ class WCS_Call_To_Action_Button_Text_Manager {
),
array(
'name' => __( 'Place Order Button Text', 'woocommerce-subscriptions' ),
'desc' => __( 'Use this field to customise the text displayed on the checkout button when an order contains a subscription. Normally the checkout submission button displays "Place order". When the cart contains a subscription, this is changed to "Sign up now".', 'woocommerce-subscriptions' ),
'desc' => __( 'Use this field to customise the text displayed on the checkout button when an order contains a subscription.', 'woocommerce-subscriptions' ),
'tip' => '',
'id' => WC_Subscriptions_Admin::$option_prefix . '_order_button_text',
'css' => 'min-width:150px;',
'default' => __( 'Add to cart', 'woocommerce-subscriptions' ),
'default' => __( 'Place order', 'woocommerce-subscriptions' ),
'type' => 'text',
'desc_tip' => true,
'placeholder' => __( 'Add to cart', 'woocommerce-subscriptions' ),
'placeholder' => __( 'Place order', 'woocommerce-subscriptions' ),
),
array(
'type' => 'sectionend',

View File

@ -357,7 +357,12 @@ class WCS_Limited_Recurring_Coupon_Manager {
$has_limited_coupon = false;
if ( $change_payment && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) ) ) {
$subscription = wcs_get_subscription( $change_payment );
$subscription = wcs_get_subscription( $change_payment );
if ( ! $subscription ) {
return $gateways;
}
$has_limited_coupon = self::order_has_limited_recurring_coupon( $subscription );
}

View File

@ -74,7 +74,10 @@ class WCS_Subscriber_Role_Manager {
),
);
WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_button_text', $role_settings, 'multiple_settings', 'sectionend' );
if ( ! WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_button_text', $role_settings, 'multiple_settings', 'sectionend' ) ) {
$settings = array_merge( $settings, $role_settings );
}
return $settings;
}

View File

@ -138,9 +138,7 @@ class WCS_Webhooks {
throw new \Exception( 'The Legacy REST API plugin is not installed on this site. More information: https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/ ' );
}
// @phpstan-ignore-next-line
WC()->api->WC_API_Subscriptions->register_routes( array() );
// @phpstan-ignore-next-line
$payload = WC()->api->WC_API_Subscriptions->get_subscription( $resource_id );
break;
case 'wp_api_v1':

View File

@ -48,6 +48,18 @@ abstract class WCS_Table_Maker {
}
}
/**
* Deletes the schema option and recreates the tables.
*
* This forces the table schema to be regenerated by removing the stored
* schema version and triggering the table registration process.
*/
public function recreate_tables() {
delete_option( $this->get_schema_option_name() );
$this->register_tables();
}
/**
* @param string $table The name of the table
*

View File

@ -519,7 +519,7 @@ class WCS_Admin_Post_Types {
'wcs-unknown-order-info-wrapper',
esc_url( 'https://woocommerce.com/document/subscriptions/store-manager-guide/#section-19' ),
// translators: Placeholder is a <br> HTML tag.
wcs_help_tip( sprintf( __( "This subscription couldn't be loaded from the database. %s Click to learn more.", 'woocommerce-subscriptions' ), '</br>' ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wcs_help_tip( sprintf( __( "This subscription couldn't be loaded from the database. %s Click to learn more.", 'woocommerce-subscriptions' ), '<br>' ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
return;
}
@ -711,12 +711,12 @@ class WCS_Admin_Post_Types {
$tooltip_classes = 'woocommerce-help-tip';
if ( $datetime->getTimestamp() < time() ) {
$tooltip_message .= __( '<b>Subscription payment overdue.</b></br>', 'woocommerce-subscriptions' );
$tooltip_message .= '<b>' . esc_html__( 'Subscription payment overdue.', 'woocommerce-subscriptions' ) . '</b><br>';
$tooltip_classes .= ' wcs-payment-overdue';
}
if ( $subscription->payment_method_supports( 'gateway_scheduled_payments' ) && ! $subscription->is_manual() ) {
$tooltip_message .= __( 'This date should be treated as an estimate only. The payment gateway for this subscription controls when payments are processed.</br>', 'woocommerce-subscriptions' );
$tooltip_message .= esc_html__( 'This date should be treated as an estimate only. The payment gateway for this subscription controls when payments are processed.', 'woocommerce-subscriptions' ) . '<br>';
$tooltip_classes .= ' wcs-offsite-renewal';
}
@ -1171,7 +1171,7 @@ class WCS_Admin_Post_Types {
// @phpstan-ignore property.notFound
foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) {
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id === $gateway_id ? 'selected' : '' ) . '>' . esc_html( $gateway->title ) . '</option>';
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id === $gateway_id ? 'selected' : '' ) . '>' . esc_html( method_exists( $gateway, 'get_title' ) ? $gateway->get_title() : $gateway->title ) . '</option>';
}
echo '<option value="_manual_renewal">' . esc_html__( 'Manual Renewal', 'woocommerce-subscriptions' ) . '</option>';
?>
@ -1503,9 +1503,20 @@ class WCS_Admin_Post_Types {
foreach ( $subscription_ids as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
$note = _x( 'Subscription status changed by bulk edit:', 'Used in order note. Reason why status changed.', 'woocommerce-subscriptions' );
$note = _x( 'Subscription status changed by bulk edit:', 'Used in order note. Reason why status changed.', 'woocommerce-subscriptions' );
try {
if ( ! $subscription ) {
throw new Exception(
sprintf(
// Translators: 1: subscription ID.
__( 'Subscription with ID %1$d does not exist and could not be updated.', 'woocommerce-subscriptions' ),
$subscription_id
)
);
}
if ( 'cancelled' === $new_status ) {
$subscription->cancel_order( $note );
} else {
@ -1542,6 +1553,11 @@ class WCS_Admin_Post_Types {
foreach ( $subscription_ids as $id ) {
$subscription = wcs_get_subscription( $id );
if ( ! $subscription ) {
continue;
}
$subscription->delete( $force_delete );
$updated_subscription = wcs_get_subscription( $id );
@ -1571,7 +1587,13 @@ class WCS_Admin_Post_Types {
foreach ( $subscription_ids as $id ) {
if ( $use_crud_method ) {
$data_store->untrash_order( wcs_get_subscription( $id ) );
$subscription = wcs_get_subscription( $id );
if ( ! $subscription ) {
continue;
}
$data_store->untrash_order( $subscription );
} else {
wp_untrash_post( $id );
}
@ -1868,7 +1890,6 @@ class WCS_Admin_Post_Types {
* @return string[] $pieces Updated associative array of clauses for the query.
*/
private function orders_table_clauses_low_performance( $pieces ) {
// @phpstan-ignore-next-line
$order_datastore = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::class );
$order_table = $order_datastore::get_orders_table_name();
$meta_table = $order_datastore::get_meta_table_name();
@ -1899,7 +1920,6 @@ class WCS_Admin_Post_Types {
private function orders_table_clauses_high_performance( $pieces ) {
global $wpdb;
// @phpstan-ignore-next-line
$order_datastore = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::class );
$order_table = $order_datastore::get_orders_table_name();
$meta_table = $order_datastore::get_meta_table_name();

View File

@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
sprintf( // Translators: The %1 placeholder is the translated order relationship ("Parent Order"), %2 placeholder is a <br> HTML tag.
__( 'This %1$s couldn\'t be loaded from the database. %1$s Click to learn more.', 'woocommerce-subscriptions' ),
esc_html( $relationship ),
'</br>'
'<br>'
)
);
?>

View File

@ -17,24 +17,6 @@ if ( ! defined( 'ABSPATH' ) ) {
class WC_Product_Subscription_Variation extends WC_Product_Variation {
/**
* A way to access the old array property.
*/
protected $subscription_variation_level_meta_data;
/**
* Create a simple subscription product object.
*
* @access public
* @param mixed $product
*/
public function __construct( $product = 0 ) {
parent::__construct( $product );
$this->subscription_variation_level_meta_data = new WCS_Array_Property_Post_Meta_Black_Magic( $this->get_id() );
}
/**
* Magic __get method for backwards compatibility. Map legacy vars to WC_Subscriptions_Product getters.
*
@ -43,19 +25,11 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
*/
public function __get( $key ) {
if ( 'subscription_variation_level_meta_data' === $key ) {
$value = wcs_product_deprecated_property_handler( $key, $this );
wcs_deprecated_argument( __CLASS__ . '::$' . $key, '2.2.0', 'Product properties should not be accessed directly with WooCommerce 3.0+. Use the getter in WC_Subscriptions_Product instead.' );
$value = $this->subscription_variation_level_meta_data; // Behold, the horror that is the magic of WCS_Array_Property_Post_Meta_Black_Magic
} else {
$value = wcs_product_deprecated_property_handler( $key, $this );
// No matching property found in wcs_product_deprecated_property_handler()
if ( is_null( $value ) ) {
$value = parent::__get( $key );
}
// No matching property found in wcs_product_deprecated_property_handler()
if ( is_null( $value ) ) {
$value = parent::__get( $key );
}
return $value;
@ -89,7 +63,6 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
/**
* Get the add to cart button text
*
* @access public
* @return string
*/
public function add_to_cart_text() {
@ -106,7 +79,6 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
/**
* Get the add to cart button text for the single page
*
* @access public
* @return string
*/
public function single_add_to_cart_text() {
@ -116,7 +88,6 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
/**
* Checks if the variable product this variation belongs to is purchasable.
*
* @access public
* @return bool
*/
public function is_purchasable() {
@ -128,12 +99,11 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
* Checks the product type to see if it is either this product's type or the parent's
* product type.
*
* @access public
* @param mixed $type Array or string of types
* @return bool
*/
public function is_type( $type ) {
if ( 'variation' == $type || ( is_array( $type ) && in_array( 'variation', $type ) ) ) {
if ( 'variation' === $type || ( is_array( $type ) && in_array( 'variation', $type, true ) ) ) {
return true;
} else {
return parent::is_type( $type );

View File

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

View File

@ -44,6 +44,14 @@ class WC_Subscription extends WC_Order {
*/
private $editable;
/**
* Stores if the subscription is in the payment completed flow.
* Allowing subscriptions to be renewed even if they have limited products.
*
* @var bool
*/
private $is_payment_completed_flow = false;
/**
* Extra data for this object. Name value pairs (name + default value). Used to add additional information to parent.
*
@ -340,7 +348,9 @@ class WC_Subscription extends WC_Order {
break;
case 'completed': // core WC order status mapped internally to avoid exceptions
case 'active':
if ( $this->payment_method_supports( 'subscription_reactivation' ) && $this->has_status( 'on-hold' ) ) {
if ( ! $this->is_payment_completed_flow && $this->contains_unavailable_product() ) {
$can_be_updated = false;
} elseif ( $this->payment_method_supports( 'subscription_reactivation' ) && $this->has_status( 'on-hold' ) ) {
// If the subscription's end date is in the past, it cannot be reactivated.
$end_time = $this->get_time( 'end' );
if ( 0 !== $end_time && $end_time < gmdate( 'U' ) ) {
@ -415,6 +425,48 @@ class WC_Subscription extends WC_Order {
return apply_filters( 'woocommerce_can_subscription_be_updated_to_' . $new_status, $can_be_updated, $this );
}
/**
* Checks if the subscription contains an unavailable product.
*
* A product is considered unavailable if it is:
* - Deleted (not found)
* - Not published (draft, trash, private, etc.)
*
* Note: This method intentionally does NOT use is_purchasable() to avoid incorrectly
* flagging limited products as unavailable. Limited products return is_purchasable() = false
* for users with existing subscriptions, but they should still be available for resubscribe.
* Functions like wcs_can_user_resubscribe_to() have specific logic to handle limited products
* by checking if the user has an active subscription.
*
* @return bool
*/
public function contains_unavailable_product() {
/** @var WC_Order_Item_Product $line_item */
foreach ( $this->get_items() as $line_item ) {
$product = $line_item->get_product();
// Product doesn't exist (deleted).
if ( ! $product instanceof WC_Product ) {
return true;
}
// If the product is a subscription variation, use the parent product.
if ( $product->is_type( 'subscription_variation' ) ) {
$parent_product_id = $product->get_parent_id();
$product = wc_get_product( $parent_product_id );
}
// Check if product is published. Products with other statuses (draft, trash, private)
// are not available for purchase or resubscribe.
$product_status = $product->get_status();
if ( 'publish' !== $product_status ) {
return true;
}
}
return false;
}
/**
* Updates status of the subscription
*
@ -1996,6 +2048,7 @@ class WC_Subscription extends WC_Order {
* @param WC_Order $last_order
*/
public function payment_complete_for_order( $last_order ) {
$this->is_payment_completed_flow = true;
// Clear the cached renewal payment counts
if ( isset( $this->cached_payment_count['completed'] ) ) {
@ -2028,30 +2081,49 @@ class WC_Subscription extends WC_Order {
}
/**
* When a payment fails, either for the original purchase or a renewal payment, this function processes it.
* When related order fails, update the status of the related order and the subscription.
* Related order can fail because of payment failure or because of other reasons.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
* @param string $new_status The new status to set for the subscription.
* @param WC_Order|bool $related_order The related order that failed payment. False if no related order is found.
* @since 7.9.0 Replaces payment_failed() method.
*/
public function payment_failed( $new_status = 'on-hold' ) {
// Make sure the last order's status is set to failed
$last_order = $this->get_last_order( 'all', 'any' );
if ( false !== $last_order ) {
$last_order->update_meta_data( self::RENEWAL_FAILED_META_KEY, wc_bool_to_string( true ) );
if ( false === $last_order->has_status( 'failed' ) ) {
remove_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment' );
$last_order->update_status( 'failed' );
add_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment', 10, 3 );
} else {
// If we didn't update the status, save the order to make sure our self::RENEWAL_FAILED_META_KEY meta data is saved.
$last_order->save();
public function payment_failed_for_related_order( $new_status = 'on-hold', $related_order = false ) {
if ( is_a( $related_order, 'WC_Order' ) ) {
$related_order_was_modified = false;
if ( wcs_order_contains_renewal( $related_order ) ) {
$related_order->update_meta_data( self::RENEWAL_FAILED_META_KEY, wc_bool_to_string( true ) );
$related_order_was_modified = true;
}
}
// Log payment failure on order
$this->add_order_note( __( 'Payment failed.', 'woocommerce-subscriptions' ) );
if ( false === $related_order->has_status( 'failed' ) ) {
remove_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment' );
// No need to set $related_order_was_modified to true here because update status will save the changes.
$related_order->update_status( 'failed' );
add_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment', 10, 3 );
} elseif ( $related_order_was_modified ) {
// If we didn't update the status, save the order to make sure our self::RENEWAL_FAILED_META_KEY meta data is saved.
$related_order->save();
}
$this->add_order_note(
sprintf(
// translators: %d: related order ID
__( 'Related order #%d failed.', 'woocommerce-subscriptions' ),
$related_order->get_id()
)
);
} else {
wc_get_logger()->debug(
'WC_Subscription::payment_failed_for_related_order is called without a related order',
array(
'subscription_id' => $this->get_id(),
'new_status' => $new_status,
'related_order' => $related_order,
'backtrace' => true,
)
);
}
// Allow a short circuit for plugins & payment gateways to force max failed payments exceeded
// This also forces the new status to be 'cancelled' if the filter is applied or the subscription is pending-cancel
@ -2068,11 +2140,23 @@ class WC_Subscription extends WC_Order {
do_action( 'woocommerce_subscription_payment_failed', $this, $new_status );
if ( false !== $last_order && wcs_order_contains_renewal( $last_order ) ) {
do_action( 'woocommerce_subscription_renewal_payment_failed', $this, $last_order );
if ( false !== $related_order && wcs_order_contains_renewal( $related_order ) ) {
do_action( 'woocommerce_subscription_renewal_payment_failed', $this, $related_order );
}
}
/**
* When a payment fails, either for the original purchase or a renewal payment, this function processes it.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
* @deprecated 7.9.0 - The method incorrectly assumes that the last order failed, and sometimes causes side effects.Use payment_failed_for_related_order instead.
*/
public function payment_failed( $new_status = 'on-hold' ) {
// Make sure the last order's status is set to failed
$last_order = $this->get_last_order( 'all', 'any' );
$this->payment_failed_for_related_order( $new_status, $last_order );
}
/*** Refund related functions are required for the Edit Order/Subscription screen, but they aren't used on a subscription ************/
/**

View File

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

View File

@ -1088,21 +1088,11 @@ class WC_Subscriptions_Cart {
self::set_cached_recurring_cart( $recurring_cart );
foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_key => $recurring_cart_package ) {
$package_index = isset( $recurring_cart_package['package_index'] ) ? $recurring_cart_package['package_index'] : 0;
$package = WC()->shipping->calculate_shipping_for_package( $recurring_cart_package );
$package_rates_match = false;
if ( isset( $standard_packages[ $package_index ] ) ) {
// phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$package_rates_match = apply_filters( 'wcs_recurring_shipping_package_rates_match_standard_rates', $package['rates'] == $standard_packages[ $package_index ]['rates'], $package['rates'], $standard_packages[ $package_index ]['rates'], $recurring_cart_key );
if ( self::package_rates_match_initial_rates( $standard_packages, $recurring_cart_package, $recurring_cart_key, $recurring_cart ) ) {
continue;
}
if ( $package_rates_match ) {
// The recurring package rates match the initial package rates, there won't be a selected shipping method for this recurring cart package move on to the next package.
if ( apply_filters( 'wcs_cart_totals_shipping_html_price_only', true, $package, $recurring_cart ) ) {
continue;
}
}
$package = WC()->shipping->calculate_shipping_for_package( $recurring_cart_package );
// If the chosen shipping method is not available for this recurring cart package, display an error and unset the selected method.
if ( ! isset( $package['rates'][ $shipping_methods[ $recurring_cart_package_key ] ] ) ) {
@ -1127,6 +1117,47 @@ class WC_Subscriptions_Cart {
self::set_cached_recurring_cart( $cached_recurring_cart );
}
/**
* Checks if the recurring package rates match the initial package rates.
*
* @param array $standard_packages The standard packages.
* @param array $recurring_cart_package The recurring cart package.
* @param string $recurring_cart_key The recurring cart key.
* @param object $recurring_cart The recurring cart.
* @return bool Whether the recurring package rates match the initial package rates.
*/
public static function package_rates_match_initial_rates( $standard_packages, $recurring_cart_package, $recurring_cart_key, $recurring_cart ) {
$package_index = isset( $recurring_cart_package['package_index'] ) ? $recurring_cart_package['package_index'] : 0;
$package = WC()->shipping->calculate_shipping_for_package( $recurring_cart_package );
$package_rates_match = false;
if ( isset( $standard_packages[ $package_index ] ) ) {
// Ignore item names during the comparison.
$package_rates = array_map(
[ __CLASS__, 'remove_item_names_from_rate' ],
$package['rates']
);
$initial_rates = array_map(
[ __CLASS__, 'remove_item_names_from_rate' ],
$standard_packages[ $package_index ]['rates']
);
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Order of array values is not important.
$package_rates_match = apply_filters( 'wcs_recurring_shipping_package_rates_match_standard_rates', $package_rates == $initial_rates, $package['rates'], $standard_packages[ $package_index ]['rates'], $recurring_cart_key );
}
if ( $package_rates_match ) {
/**
* The recurring package rates match the initial package rates, there won't be a selected shipping method for this recurring cart package move on to the next package.
*
* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
*/
return apply_filters( 'wcs_cart_totals_shipping_html_price_only', true, $package, $recurring_cart );
}
return false;
}
/**
* Checks the cart to see if it contains a specific product.
*
@ -1159,19 +1190,20 @@ class WC_Subscriptions_Cart {
*/
public static function cart_contains_other_subscription_products( $product_id ) {
$cart_contains_other_subscription_products = false;
if ( empty( WC()->cart->cart_contents ) || ! WC_Subscriptions_Product::is_subscription( $product_id ) ) {
return false;
}
if ( ! empty( WC()->cart->cart_contents ) && WC_Subscriptions_Product::is_subscription( $product_id ) ) {
$is_subscription = WC_Subscriptions_Product::is_subscription( $product_id );
foreach ( WC()->cart->cart_contents as $cart_item ) {
if ( wcs_get_canonical_product_id( $cart_item ) !== $product_id ) {
$cart_contains_other_subscription_products = true;
break;
}
foreach ( WC()->cart->cart_contents as $cart_item ) {
$item_product_id = wcs_get_canonical_product_id( $cart_item );
$is_subscription = isset( $item_product_id ) ? WC_Subscriptions_Product::is_subscription( $item_product_id ) : false;
if ( $item_product_id !== $product_id && $is_subscription ) {
return true;
}
}
return $cart_contains_other_subscription_products;
// Return false because no other subscription product was found in the cart.
return false;
}
/**
@ -1485,7 +1517,7 @@ class WC_Subscriptions_Cart {
*/
public static function pre_get_refreshed_fragments() {
wcs_deprecated_function( __METHOD__, '2.5.0' );
if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) {
if ( wp_doing_ajax() && ! defined( 'WOOCOMMERCE_CART' ) ) {
define( 'WOOCOMMERCE_CART', true );
WC()->cart->calculate_totals();
}
@ -2538,4 +2570,17 @@ class WC_Subscriptions_Cart {
return true;
}
/**
* Remove the item names from the shipping rate
* to check if the rates match.
*
* @param WC_Shipping_Rate $rate The rate.
* @return WC_Shipping_Rate The rate.
*/
private static function remove_item_names_from_rate( $rate ) {
$rate->add_meta_data( 'Items', null );
return $rate;
}
}

View File

@ -139,6 +139,11 @@ class WC_Subscriptions_Change_Payment_Gateway {
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
$subscription_key = isset( $_GET['key'] ) ? wc_clean( $_GET['key'] ) : '';
if ( ! $subscription ) {
wc_print_notice( __( 'There was an unexpected problem with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
return;
}
do_action( 'before_woocommerce_pay' );
/**
@ -276,19 +281,27 @@ class WC_Subscriptions_Change_Payment_Gateway {
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v1.4
*/
public static function change_payment_method_via_pay_shortcode() {
if ( ! isset( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_POST['_wcsnonce'] ) ), 'wcs_change_payment_method' ) ) {
return;
}
$subscription_id = absint( wc_clean( wp_unslash( $_POST['woocommerce_change_payment'] ) ) );
$subscription = wcs_get_subscription( $subscription_id );
$subscription = wcs_get_subscription( $subscription_id );
if ( ! $subscription ) {
wc_add_notice( __( 'There was an unexpected problem with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
return;
}
do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription );
ob_start();
if ( ! $subscription instanceof WC_Subscription || $subscription->get_order_key() !== wc_clean( wp_unslash( $_GET['key'] ?? '' ) ) ) {
return;
}
if ( $subscription->get_order_key() == wc_clean( wp_unslash( $_GET['key'] ) ) ) {
try {
// We open an output buffer to suppress any error noise that might break the redirect.
$output_buffer_opened = ob_start();
$subscription_billing_country = $subscription->get_billing_country();
$subscription_billing_state = $subscription->get_billing_state();
@ -359,6 +372,11 @@ class WC_Subscriptions_Change_Payment_Gateway {
*/
$subscription = wcs_get_subscription( $subscription->get_id() );
if ( ! $subscription ) {
wc_add_notice( __( 'There was an unexpected problem with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
return;
}
$subscription->set_requires_manual_renewal( false );
$subscription->save();
@ -383,9 +401,13 @@ class WC_Subscriptions_Change_Payment_Gateway {
wp_safe_redirect( $result['redirect'] );
exit;
}
} finally {
// Close the output buffer (if we exited, this will have happened automatically, so this is primarily here
// to ensure we clean-up in the event that we returned early).
if ( $output_buffer_opened ) {
ob_clean();
}
}
ob_get_clean();
}
/**
@ -595,7 +617,12 @@ class WC_Subscriptions_Change_Payment_Gateway {
*/
if ( ! $is_change_payment_method_request && $cart_contains_failed_renewal ) {
$subscription = wcs_get_subscription( $renewal_order_cart_item['subscription_renewal']['subscription_id'] );
$cart_contains_failed_manual_renewal = $subscription->is_manual();
// If the subscription has been deleted (which is a reason $subscription may be false), we probably do not
// need to concern ourselves with a failed manual renewal.
if ( $subscription ) {
$cart_contains_failed_manual_renewal = $subscription->is_manual();
}
}
if ( apply_filters( 'wcs_payment_gateways_change_payment_method', $is_change_payment_method_request || ( $cart_contains_failed_renewal && ! $cart_contains_failed_manual_renewal ) ) ) {
@ -728,6 +755,7 @@ class WC_Subscriptions_Change_Payment_Gateway {
}
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
if ( ! $subscription ) {
return $title;
}
@ -823,19 +851,19 @@ class WC_Subscriptions_Change_Payment_Gateway {
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
if ( $subscription ) {
wc_add_notice( __( 'Please log in to your account below to choose a new payment method for your subscription.', 'woocommerce-subscriptions' ), 'notice' );
ob_start();
woocommerce_login_form( array(
'redirect' => $subscription->get_change_payment_method_url(),
'message' => wc_print_notices( true ),
) );
$content = ob_get_clean();
if ( ! $subscription ) {
return $content;
}
return $content;
wc_add_notice( __( 'Please log in to your account below to choose a new payment method for your subscription.', 'woocommerce-subscriptions' ), 'notice' );
ob_start();
woocommerce_login_form( array(
'redirect' => $subscription->get_change_payment_method_url(),
'message' => wc_print_notices( true ),
) );
return ob_get_clean();
}
/** Deprecated Functions **/

View File

@ -656,13 +656,13 @@ class WC_Subscriptions_Checkout {
}
/**
* Filter the "Add to cart" button text for subscription carts.
* Filter the "Place order" button text for subscription carts.
*
* @since 7.8.0
* @param string $button_text The "Add to cart" button text.
* @return string The "Add to cart" button text.
* @param string $button_text The "Place order" button text.
* @return string The "Place order" button text.
*/
return apply_filters( 'wcs_place_subscription_order_text', __( 'Add to cart', 'woocommerce-subscriptions' ) );
return apply_filters( 'wcs_place_subscription_order_text', __( 'Place order', 'woocommerce-subscriptions' ) );
}
/**

View File

@ -542,6 +542,9 @@ class WC_Subscriptions_Core_Plugin {
update_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$switch_setting_string, 'yes' );
}
// Setup downloads table.
WC_Subscription_Downloads::install();
update_option( WC_Subscriptions_Admin::$option_prefix . '_is_active', true );
set_transient( $this->get_activation_transient(), true, 60 * 60 );

View File

@ -83,8 +83,8 @@ class WC_Subscriptions_Coupon {
// Add our recurring product coupon types to the list of coupon types that apply to individual products
add_filter( 'woocommerce_product_coupon_types', __CLASS__ . '::filter_product_coupon_types', 10, 1 );
if ( ! is_admin() ) {
// WC 3.0 only sets a coupon type if it is a pre-defined supported type, so we need to temporarily add our pseudo types. We don't want to add these on admin pages.
// Adds pseudo coupons on every page so orders can be created and renewed with them but prevent showing them on the coupon edit page.
if ( ! self::is_coupon_edit_page() ) {
add_filter( 'woocommerce_coupon_discount_types', __CLASS__ . '::add_pseudo_coupon_types' );
}
@ -748,6 +748,29 @@ class WC_Subscriptions_Coupon {
return isset( self::$recurring_coupons[ $coupon_type ] );
}
/**
* Check if the current page is the coupon edit page.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v4.0.0
*
* @return bool Whether the current page is the coupon edit page.
*/
public static function is_coupon_edit_page() {
global $pagenow;
$current_post_type = '';
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( 'post.php' === $pagenow && isset( $_GET['post'] ) ) {
$current_post_type = get_post_type( $_GET['post'] );
} elseif ( isset( $_GET['post_type'] ) ) {
$current_post_type = $_GET['post_type'];
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
return 'shop_coupon' === $current_post_type;
}
/* Deprecated */
/**

View File

@ -156,6 +156,11 @@ class WC_Subscriptions_Email_Notifications {
switch ( current_action() ) {
case 'woocommerce_scheduled_subscription_customer_notification_renewal':
$subscription = wcs_get_subscription( $subscription_id );
if ( ! $subscription ) {
break;
}
if ( $subscription->get_total() <= 0 ) {
break;
}
@ -167,6 +172,11 @@ class WC_Subscriptions_Email_Notifications {
break;
case 'woocommerce_scheduled_subscription_customer_notification_trial_expiration':
$subscription = wcs_get_subscription( $subscription_id );
if ( ! $subscription ) {
break;
}
if ( $subscription->is_manual() ) {
$notification = $emails['WCS_Email_Customer_Notification_Manual_Trial_Expiration'];
} else {
@ -268,7 +278,7 @@ class WC_Subscriptions_Email_Notifications {
$notification_settings = [
[
'name' => __( 'Customer Notifications', 'woocommerce-subscriptions' ),
'name' => __( 'Customer notifications', 'woocommerce-subscriptions' ),
'type' => 'title',
'id' => WC_Subscriptions_Admin::$option_prefix . '_customer_notifications',
/* translators: Link to WC Settings > Email. */
@ -304,7 +314,9 @@ class WC_Subscriptions_Email_Notifications {
],
];
WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_miscellaneous', $notification_settings, 'multiple_settings', 'sectionend' );
if ( ! WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_miscellaneous', $notification_settings, 'multiple_settings', 'sectionend' ) ) {
$settings = array_merge( $settings, $notification_settings );
}
return $settings;
}
}

View File

@ -60,6 +60,8 @@ class WC_Subscriptions_Extend_Store_Endpoint {
// @phpstan-ignore class.notFound
self::$currency_formatter = function_exists( 'woocommerce_store_api_get_formatter' ) ? woocommerce_store_api_get_formatter( 'currency' ) : Package::container()->get( ExtendRestApi::class )->get_formatter( 'currency' );
self::extend_store();
add_action( 'woocommerce_store_api_cart_select_shipping_rate', [ __CLASS__, 'initial_shipment_select_shipping_rate' ], 10, 2 );
}
/**
@ -273,9 +275,10 @@ class WC_Subscriptions_Extend_Store_Endpoint {
// Add extra package data to array.
if ( count( $packages ) ) {
$packages = array_map(
function( $key, $package, $index ) use ( $cart ) {
function ( $key, $package, $index ) use ( $cart ) {
$package['package_id'] = isset( $package['package_id'] ) ? $package['package_id'] : $key;
$package['package_name'] = isset( $package['package_name'] ) ? $package['package_name'] : self::get_shipping_package_name( $package, $cart );
$package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package );
return $package;
},
array_keys( $packages ),
@ -331,6 +334,7 @@ class WC_Subscriptions_Extend_Store_Endpoint {
}
$future_subscriptions = array();
$standard_packages = WC()->shipping->get_packages();
if ( ! empty( wc()->cart->recurring_carts ) ) {
foreach ( wc()->cart->recurring_carts as $cart_key => $cart ) {
@ -359,7 +363,18 @@ class WC_Subscriptions_Extend_Store_Endpoint {
'tax_lines' => self::get_tax_lines( $cart ),
)
),
'shipping_rates' => array_values( array_map( array( self::$schema->get( 'cart-shipping-rate' ), 'get_item_response' ), $shipping_packages ) ),
'shipping_rates' => array_values(
array_map(
function ( $package ) use ( $cart, $cart_key, $standard_packages ) {
$shipping_package = self::$schema->get( 'cart-shipping-rate' )->get_item_response( $package );
$shipping_package['match_initial_rates'] = WC_Subscriptions_Cart::package_rates_match_initial_rates( $standard_packages, $package, $cart_key, $cart );
$shipping_package['needs_shipping'] = WC_Subscriptions_Cart::cart_contains_subscriptions_needing_shipping( $cart );
return $shipping_package;
},
$shipping_packages
)
),
);
}
}
@ -367,6 +382,42 @@ class WC_Subscriptions_Extend_Store_Endpoint {
return $future_subscriptions;
}
/**
* Select the initial shipment shipping rate.
*
* @param string $package_id Package ID.
* @param string $rate_id Rate ID.
*/
public static function initial_shipment_select_shipping_rate( $package_id, $rate_id ) {
WC()->cart->calculate_totals();
$standard_packages = WC()->shipping->get_packages();
if ( ! is_numeric( $package_id ) || key( $standard_packages ) !== (int) $package_id ) {
return;
}
foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) {
if ( false === $recurring_cart->needs_shipping() || 0 === $recurring_cart->next_payment_date ) {
continue;
}
WC_Subscriptions_Cart::set_calculation_type( 'recurring_total' );
WC_Subscriptions_Cart::set_recurring_cart_key( $recurring_cart_key );
WC_Subscriptions_Cart::set_cached_recurring_cart( $recurring_cart );
foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_key => $recurring_cart_package ) {
if ( WC_Subscriptions_Cart::package_rates_match_initial_rates( $standard_packages, $recurring_cart_package, $recurring_cart_key, $recurring_cart ) ) {
$session_data = wc()->session->get( 'chosen_shipping_methods' ) ? wc()->session->get( 'chosen_shipping_methods' ) : [];
$session_data[ $recurring_cart_package_key ] = $rate_id;
wc()->session->set( 'chosen_shipping_methods', $session_data );
}
}
}
}
/**
* Format sign-up fees.
*

View File

@ -34,11 +34,13 @@ class WC_Subscriptions_Frontend_Scripts {
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v3.1.3
*/
public static function enqueue_scripts() {
global $post;
$dependencies = array( 'jquery' );
if ( is_cart() || is_checkout() ) {
wp_enqueue_script( 'wcs-cart', self::get_file_url( 'assets/js/frontend/wcs-cart.js' ), $dependencies, WC_Subscriptions_Core_Plugin::instance()->get_library_version(), true );
} elseif ( is_product() ) {
} elseif ( is_product() && WC_Subscriptions_Product::is_subscription( $post->ID ) ) {
wp_enqueue_script( 'wcs-single-product', self::get_file_url( 'assets/js/frontend/single-product.js' ), $dependencies, WC_Subscriptions_Core_Plugin::instance()->get_library_version(), true );
} elseif ( wcs_is_view_subscription_page() ) {
$subscription = wcs_get_subscription( absint( get_query_var( 'view-subscription' ) ) );
@ -66,6 +68,7 @@ class WC_Subscriptions_Frontend_Scripts {
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v3.1.3
*/
public static function enqueue_styles( $styles ) {
global $post;
if ( is_checkout() || is_cart() ) {
$styles['wcs-checkout'] = array(
@ -82,6 +85,35 @@ class WC_Subscriptions_Frontend_Scripts {
'media' => 'all',
);
}
if (
wp_is_block_theme() ||
(
! empty( $post ) &&
(
WC_Blocks_Utils::has_block_in_page( $post->ID, 'woocommerce/cart' ) ||
WC_Blocks_Utils::has_block_in_page( $post->ID, 'woocommerce/mini-cart' ) ||
WC_Blocks_Utils::has_block_in_page( $post->ID, 'woocommerce/checkout' )
)
)
) {
$styles['wcs-blocks-integration'] = array(
'src' => \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( 'build/index.css' ),
'deps' => 'wc-checkout',
'version' => WCS_Blocks_Integration::get_file_version( \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/index.css' ) ),
'media' => 'all',
);
if ( WCSG_Admin::is_gifting_enabled() ) {
$styles['wcsg-blocks-integration'] = array(
'src' => \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( 'build/wcsg-blocks-integration.css' ),
'deps' => 'wc-checkout',
'version' => WCS_Blocks_Integration::get_file_version( \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/wcsg-blocks-integration.css' ) ),
'media' => 'all',
);
}
}
return $styles;
}
}

View File

@ -130,11 +130,10 @@ class WC_Subscriptions_Manager {
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.12
*/
public static function process_renewal( $subscription_id, $required_status, $order_note ) {
$subscription = wcs_get_subscription( $subscription_id );
// If the subscription is using manual payments, the gateway isn't active or it manages scheduled payments
if ( ! empty( $subscription ) && $subscription->has_status( $required_status ) && ( 0 == $subscription->get_total() || $subscription->is_manual() || '' == $subscription->get_payment_method() || ! $subscription->payment_method_supports( 'gateway_scheduled_payments' ) ) ) {
if ( $subscription instanceof WC_Subscription && $subscription->has_status( $required_status ) && ( 0 === absint( $subscription->get_total() ) || $subscription->is_manual() || '' === $subscription->get_payment_method() || ! $subscription->payment_method_supports( 'gateway_scheduled_payments' ) ) ) {
// Always put the subscription on hold in case something goes wrong while trying to process renewal
$subscription->update_status( 'on-hold', $order_note );
@ -438,11 +437,13 @@ class WC_Subscriptions_Manager {
/**
* Called when a sign up fails during the payment processing step.
*
* This method only performs actions when a parent order changed to failed status.
* Overlaps with WC_Subscriptions_Order::maybe_record_subscription_payment.
*
* @param WC_Order|int $order The order or ID of the order for which subscriptions should be marked as failed.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v1.0
*/
public static function failed_subscription_sign_ups_for_order( $order ) {
$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'parent' ) );
if ( empty( $subscriptions ) ) {
@ -461,9 +462,23 @@ class WC_Subscriptions_Manager {
foreach ( $subscriptions as $subscription ) {
try {
$new_status = $subscription->has_status( 'pending' ) && $subscription->get_parent_id() === $order->get_id() ? 'pending' : 'on-hold';
$subscription->payment_failed( $new_status );
/**
* Parent status failure should not change the subscription status in most cases.
* We only change the status when the parent order is the last order and subscription is active.
* In that case, we need to put the subscription on hold as we do when the latest renewal order fails.
* Examples:
* - if subscription is pending and initial payment fails, we keep subscription as pending to activate it on successful payment.
* - if subscription has successful renewals and somehow the parent order fails after that, we still keep subscription as active.
* - if subscription is active after successful initial payment, but later the parent order fails, we move subscription to on-hold.
*/
$new_status = $subscription->get_status();
if ( 'active' === $new_status && $subscription->get_parent_id() === $order->get_id() ) {
$last_order = $subscription->get_last_order( 'ids', 'any' );
$new_status = $last_order === $order->get_id() ? 'on-hold' : 'active';
}
$subscription->payment_failed_for_related_order( $new_status, $order );
} catch ( Exception $e ) {
// translators: $1: order number, $2: error message
$subscription->add_order_note( sprintf( __( 'Failed to process failed payment on subscription for order #%1$s: %2$s', 'woocommerce-subscriptions' ), is_object( $order ) ? $order->get_order_number() : $order, $e->getMessage() ) );
@ -1020,6 +1035,24 @@ class WC_Subscriptions_Manager {
foreach ( $subscriptions as $subscription ) {
$subscription_number = $subscription->get_order_number();
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
// translators: %s is the email address of the recipient.
$message = sprintf( __( 'The gifted subscription moved to the purchaser after the recipient user with email address %s was deleted.', 'woocommerce-subscriptions' ), $subscription->get_meta( '_recipient_user_email_address' ) );
$subscription->delete_meta_data( '_recipient_user' );
$subscription->delete_meta_data( '_recipient_user_email_address' );
$subscription->save();
$subscription->add_order_note( $message, $subscription_number );
// Check needed for tests.
if ( $subscription->get_parent() ) {
$subscription->get_parent()->add_order_note( $message, $subscription_number );
}
continue;
}
// Before deleting the subscription, add an order note to the related orders.
foreach ( $subscription->get_related_orders( 'all', array( 'parent', 'renewal', 'switch' ) ) as $order ) {
if ( $current_user ) {
@ -2041,6 +2074,11 @@ class WC_Subscriptions_Manager {
*/
public static function maybe_process_failed_renewal_for_repair( $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( ! $subscription ) {
return;
}
if ( 'true' === $subscription->get_meta( '_wcs_repaired_2_0_2_needs_failed_payment', true ) ) {
// Always put the subscription on hold in case something goes wrong while trying to process renewal
$subscription->update_status( 'on-hold', _x( 'Subscription renewal payment due:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) );

View File

@ -485,7 +485,7 @@ class WC_Subscriptions_Order {
* @param string $new_order_status The new order status.
*/
public static function maybe_record_subscription_payment( $order_id, $old_order_status, $new_order_status ) {
// ! This hook will only perform actions on parent order status changes.
if ( ! wcs_order_contains_subscription( $order_id, 'parent' ) ) {
return;
}
@ -578,10 +578,6 @@ class WC_Subscriptions_Order {
$subscription->payment_complete_for_order( $order );
$was_activated = true;
} elseif ( 'failed' === $new_order_status ) {
// When parent order fails, we want to keep the subscription status as pending unless it had another status before.
$new_status = $subscription->has_status( 'pending' ) && $subscription->get_parent_id() === $order->get_id() ? 'pending' : 'on-hold';
$subscription->payment_failed( $new_status );
}
}
@ -2482,7 +2478,6 @@ f *
if ( isset( $order->$meta_key ) ) { // WC 2.1+ magic __isset() & __get() methods
$meta_value = $order->$meta_key;
// @phpstan-ignore-next-line
} elseif ( is_array( $order->order_custom_fields ) && isset( $order->order_custom_fields[ '_' . $meta_key ][0] ) && $order->order_custom_fields[ '_' . $meta_key ][0] ) { // < WC 2.1+
$meta_value = maybe_unserialize( $order->order_custom_fields[ '_' . $meta_key ][0] );
} else {

View File

@ -1281,18 +1281,6 @@ class WC_Subscriptions_Product {
return $button_text;
}
/**
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
* @return bool
*/
public static function is_purchasable( $is_purchasable, $product ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_product' );
return WCS_Limiter::is_purchasable_product( $is_purchasable, $product );
}
/**
* Check if the current session has an order awaiting payment for a subscription to a specific product line item.
*

View File

@ -81,10 +81,12 @@ class WC_Subscriptions_Renewal_Order {
* subscriptions are updated even if payment is processed by a manual payment gateways (which would never trigger the
* 'woocommerce_payment_complete' hook) or by some other means that circumvents that hook.
*
* This hook will be skipped for early renewal orders transitioning to statuses other than cancelled or refunded.
* @see WCS_Cart_Early_Renewal::maybe_record_subscription_payment().
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
*/
public static function maybe_record_subscription_payment( $order_id, $orders_old_status, $orders_new_status ) {
if ( ! wcs_order_contains_renewal( $order_id ) ) {
return;
}

View File

@ -197,7 +197,7 @@ class WC_Subscriptions_Synchroniser {
public static function add_settings( $settings ) {
$synchronisation_settings = array(
array(
'name' => __( 'Synchronisation', 'woocommerce-subscriptions' ),
'name' => __( 'Synchronization', 'woocommerce-subscriptions' ),
'type' => 'title',
// translators: placeholders are opening and closing link tags
'desc' => sprintf( _x( 'Align subscription renewal to a specific day of the week, month or year. For example, the first day of the month. %1$sLearn more%2$s.', 'used in the general subscription options page', 'woocommerce-subscriptions' ), '<a href="' . esc_url( 'https://woocommerce.com/document/subscriptions/renewal-synchronisation/' ) . '">', '</a>' ),

View File

@ -5,12 +5,17 @@
* @class WC_Subscriptions_Tracker
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.6.4
* @package WooCommerce Subscriptions/Classes
* @category Class
*/
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce_Subscriptions\Internal\Telemetry\Collector;
class WC_Subscriptions_Tracker {
/**
* Handles the collection of additional pieces of data.
*/
private static Collector $telemetry_collector;
/**
* Initialize the Tracker.
@ -18,6 +23,8 @@ class WC_Subscriptions_Tracker {
public static function init() {
// Only add data if Tracker enabled
if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) {
self::$telemetry_collector = new Collector();
self::$telemetry_collector->setup();
add_filter( 'woocommerce_tracker_data', [ __CLASS__, 'add_subscriptions_tracking_data' ], 10, 1 );
}
}
@ -32,6 +39,11 @@ class WC_Subscriptions_Tracker {
$data['extensions']['wc_subscriptions']['settings'] = self::get_subscriptions_options();
$data['extensions']['wc_subscriptions']['subscriptions'] = self::get_subscriptions();
$data['extensions']['wc_subscriptions']['subscription_orders'] = self::get_subscription_orders();
// Insert any additional telemetry that we have been collecting.
$additional_telemetry = self::$telemetry_collector->get_telemetry_data();
$data['extensions']['wc_subscriptions'] = array_merge_recursive( $data['extensions']['wc_subscriptions'], $additional_telemetry );
return $data;
}
@ -41,47 +53,86 @@ class WC_Subscriptions_Tracker {
* @return array Subscriptions options data.
*/
private static function get_subscriptions_options() {
$customer_notifications_offset = get_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$offset_setting_string, [] );
return [
// Staging and live site
'wc_subscriptions_staging' => WCS_Staging::is_duplicate_site() ? 'staging' : 'live',
'wc_subscriptions_live_url' => esc_url( WCS_Staging::get_site_url_from_source( 'subscriptions_install' ) ),
// Button text, roles
// Button text
// Add to Cart Button Text
'add_to_cart_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text' ),
// Place Order Button Text
'order_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_order_button_text' ),
// Roles
// Subscriber Default Role
'subscriber_role' => get_option( WC_Subscriptions_Admin::$option_prefix . '_subscriber_role' ),
// Inactive Subscriber Role
'cancelled_role' => get_option( WC_Subscriptions_Admin::$option_prefix . '_cancelled_role' ),
// Renewals
// Accept Manual Renewals
'accept_manual_renewals' => get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals' ),
// Turn off Automatic Payments
'turn_off_automatic_payments' => 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'none' ),
// Auto Renewal Toggle
'enable_auto_renewal_toggle' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_auto_renewal_toggle' ),
// Early renewal
// Accept Early Renewal Payments
'enable_early_renewal' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal' ),
// Accept Early Renewal Payments via a Modal
'enable_early_renewal_via_modal' => 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal_via_modal', 'none' ),
// Switching
// Between Subscription Variations and Between Grouped Subscriptions are condensed into this setting.
'allow_switching' => get_option( WC_Subscriptions_Admin::$option_prefix . '_allow_switching' ),
// Prorate Recurring Payment
'apportion_recurring_price' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_recurring_price', 'none' ),
// Prorate Sign up Fee
'apportion_sign_up_fee' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_sign_up_fee', 'none' ),
// Prorate Subscription Length
'apportion_length' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_length', 'none' ),
// Switch Button Text
'switch_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_switch_button_text', 'none' ),
// Gifting
// Enable gifting
'gifting_enable_gifting' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_enable_gifting' ),
'gifting_default_option' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_default_option' ),
// Gifting Checkbox Text
'gifting_gifting_checkbox_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_gifting_checkbox_text' ),
// Downloadable Products
'gifting_downloadable_products' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_downloadable_products' ),
// Synchronization
// Synchronise renewals
'sync_payments' => get_option( WC_Subscriptions_Admin::$option_prefix . '_sync_payments' ),
// Prorate First Renewal
'prorate_synced_payments' => $prorate_synced_payments = ( 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_sync_payments' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_prorate_synced_payments', 'none' ) ),
// Sign-up grace period
'days_no_fee' => 'recurring' === $prorate_synced_payments ? get_option( WC_Subscriptions_Admin::$option_prefix . '_days_no_fee', 'none' ) : 'none',
// Miscellaneous
// Customer Suspensions
'max_customer_suspensions' => get_option( WC_Subscriptions_Admin::$option_prefix . '_max_customer_suspensions' ),
// Mixed Checkout
'multiple_purchase' => get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase' ),
// $0 Initial Checkout
'allow_zero_initial_order_without_payment_method' => get_option( WC_Subscriptions_Admin::$option_prefix . '_zero_initial_payment_requires_payment' ),
// Drip Downloadable Content
'drip_downloadable_content_on_renewal' => get_option( WC_Subscriptions_Admin::$option_prefix . '_drip_downloadable_content_on_renewal' ),
// Retry Failed Payments
'enable_retry' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_retry' ),
// Notifications
'enable_notification_reminders' => get_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$switch_setting_string, 'no' ),
// Enable Reminders
'enable_notification_reminders' => get_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$switch_setting_string ),
// Reminder Timing
'customer_notifications_offset_number' => $customer_notifications_offset['number'] ?? 'none',
'customer_notifications_offset_unit' => $customer_notifications_offset['unit'] ?? 'none',
];
}
@ -119,80 +170,21 @@ class WC_Subscriptions_Tracker {
* @return array Subscription order counts and totals by type (initial, switch, renewal, resubscribe). Values are returned as strings.
*/
private static function get_subscription_orders() {
$order_totals = [];
$relation_types = [
'switch',
'renewal',
'resubscribe',
];
$order_totals = array();
// Get the subtotal and count for each subscription type.
foreach ( $relation_types as $relation_type ) {
// Get orders with the given relation type.
$relation_orders = wcs_get_orders_with_meta_query(
[
'type' => 'shop_order',
'status' => [ 'wc-completed', 'wc-processing', 'wc-refunded' ],
'limit' => -1,
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
[
'key' => '_subscription_' . $relation_type,
'compare' => 'EXISTS',
],
],
]
);
// Get the subtotal and count for all subscription types in one query
$counts_and_totals = self::get_order_count_and_total_by_meta_key();
// Sum the totals and count the orders.
$count = count( $relation_orders );
$total = array_reduce(
$relation_orders,
function( $total, $order ) {
return $total + $order->get_total();
},
0
);
$order_totals[ $relation_type . '_gross' ] = $total;
$order_totals[ $relation_type . '_count' ] = $count;
foreach ( $counts_and_totals as $type => $data ) {
$order_totals[ $type . '_gross' ] = $data['total'];
$order_totals[ $type . '_count' ] = $data['count'];
}
// Finally, get the initial revenue and count.
// Get the orders for all initial subscription orders (no switch, renewal or resubscribe meta key).
$initial_subscription_orders = wcs_get_orders_with_meta_query(
[
'type' => 'shop_order',
'status' => [ 'wc-completed', 'wc-processing', 'wc-refunded' ],
'limit' => -1,
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
[
'key' => '_subscription_switch',
'compare' => 'NOT EXISTS',
],
[
'key' => '_subscription_renewal',
'compare' => 'NOT EXISTS',
],
[
'key' => '_subscription_resubscribe',
'compare' => 'NOT EXISTS',
],
],
]
);
// Get initial orders (orders without switch, renewal, or resubscribe meta keys).
$count_and_total = self::get_initial_order_count_and_total();
// Sum the totals and count the orders.
$initial_order_count = count( $initial_subscription_orders );
$initial_order_total = array_reduce(
$initial_subscription_orders,
function( $total, $order ) {
return $total + $order->get_total();
},
0
);
$order_totals['initial_gross'] = $initial_order_total;
$order_totals['initial_count'] = $initial_order_count;
$order_totals['initial_gross'] = $count_and_total['total'];
$order_totals['initial_count'] = $count_and_total['count'];
// Ensure all values are strings.
$order_totals = array_map( 'strval', $order_totals );
@ -200,6 +192,149 @@ class WC_Subscriptions_Tracker {
return $order_totals;
}
/**
* Gets order count and total for subscription-related orders.
*
* @return array Array with counts and totals for switch, renewal, and resubscribe orders.
*/
private static function get_order_count_and_total_by_meta_key() {
global $wpdb;
$order_statuses = array( 'wc-completed', 'wc-refunded' );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
if ( wcs_is_custom_order_tables_usage_enabled() ) {
// HPOS: Use wc_orders and wc_orders_meta tables.
$orders_table = $wpdb->prefix . 'wc_orders';
$meta_table = $wpdb->prefix . 'wc_orders_meta';
$query = $wpdb->prepare(
"SELECT
order_relation.meta_key as 'type',
SUM( orders.total_amount ) AS 'total',
COUNT( orders.id ) as 'count'
FROM {$orders_table} AS orders
RIGHT JOIN {$meta_table} AS order_relation ON order_relation.order_id = orders.id
WHERE order_relation.meta_key IN ( '_subscription_switch', '_subscription_renewal', '_subscription_resubscribe' )
AND orders.status in (%s, %s)
AND orders.type = 'shop_order'
GROUP BY order_relation.meta_key",
$order_statuses
);
} else {
// CPT: Use posts and postmeta tables.
$query = $wpdb->prepare(
"SELECT
order_relation.meta_key as 'type',
SUM( order_total.meta_value ) AS 'total',
COUNT( orders.ID ) as 'count'
FROM {$wpdb->prefix}posts AS orders
RIGHT JOIN {$wpdb->prefix}postmeta AS order_relation ON order_relation.post_id = orders.ID
RIGHT JOIN {$wpdb->prefix}postmeta AS order_total ON order_total.post_id = orders.ID
WHERE order_relation.meta_key IN ( '_subscription_switch', '_subscription_renewal', '_subscription_resubscribe' )
AND orders.post_status in (%s, %s)
AND order_total.meta_key = '_order_total'
GROUP BY order_relation.meta_key",
$order_statuses
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
$results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$totals = array(
'switch' => array(
'count' => 0,
'total' => 0.0,
),
'renewal' => array(
'count' => 0,
'total' => 0.0,
),
'resubscribe' => array(
'count' => 0,
'total' => 0.0,
),
);
if ( $results ) {
foreach ( $results as $result ) {
$type = str_replace( '_subscription_', '', $result['type'] );
if ( isset( $totals[ $type ] ) ) {
$totals[ $type ] = array(
'count' => (int) $result['count'],
'total' => (float) $result['total'],
);
}
}
}
// Log if any type has no data
foreach ( $totals as $type => $data ) {
if ( 0 === $data['count'] && 0.0 === $data['total'] ) {
wc_get_logger()->warning( "WC_Subscriptions_Tracker::get_order_count_and_total_by_meta_key() returned 0 count and total for {$type} orders" );
}
}
return $totals;
}
/**
* Gets count and total for initial orders (orders without subscription relation meta keys).
*
* @return array Array with 'count' and 'total' keys.
*/
private static function get_initial_order_count_and_total() {
global $wpdb;
$order_statuses = array( 'wc-completed', 'wc-refunded' );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
if ( wcs_is_custom_order_tables_usage_enabled() ) {
// HPOS: Use wc_orders table
$orders_table = $wpdb->prefix . 'wc_orders';
$query = $wpdb->prepare(
"SELECT
SUM( orders.total_amount ) AS 'total', COUNT( DISTINCT orders.id ) as 'count'
FROM {$orders_table} AS orders
RIGHT JOIN {$orders_table} AS subscriptions ON subscriptions.parent_order_id = orders.id
WHERE orders.status in ( %s, %s )
AND subscriptions.type = 'shop_subscription'
AND orders.type = 'shop_order'",
$order_statuses
);
} else {
// CPT: Use posts and postmeta tables.
$query = $wpdb->prepare(
"SELECT
SUM( order_total.meta_value ) AS 'total', COUNT( * ) as 'count'
FROM {$wpdb->posts} AS orders
RIGHT JOIN {$wpdb->posts} AS subscriptions ON subscriptions.post_parent = orders.ID
RIGHT JOIN {$wpdb->postmeta} AS order_total ON order_total.post_id = orders.ID
WHERE orders.post_status in ( %s, %s )
AND subscriptions.post_type = 'shop_subscription'
AND orders.post_type = 'shop_order'
AND order_total.meta_key = '_order_total'",
$order_statuses
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
$result = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$totals = array(
'count' => (int) ( $result ? $result['count'] : 0 ),
'total' => (float) ( $result ? $result['total'] : 0 ),
);
if ( 0 === $totals['count'] && 0.0 === $totals['total'] ) {
wc_get_logger()->warning( 'WC_Subscriptions_Tracker::get_initial_order_count_and_total() returned 0 count and total' );
}
return $totals;
}
/**
* Gets first and last subscription created dates.
*

View File

@ -22,17 +22,15 @@ class WCS_Blocks_Integration implements IntegrationInterface {
*/
public function initialize() {
$script_path = 'build/index.js';
$style_path = 'build/index.css';
$script_url = \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( $script_path );
$style_url = \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( $style_path );
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/index.asset.php' );
$script_asset = file_exists( $script_asset_path )
? require $script_asset_path
: array(
'dependencies' => array(),
'version' => $this->get_file_version( $script_asset_path ),
'version' => self::get_file_version( $script_asset_path ),
);
wp_register_script(
@ -47,13 +45,6 @@ class WCS_Blocks_Integration implements IntegrationInterface {
'woocommerce-subscriptions',
\WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'languages' )
);
wp_enqueue_style(
'wc-blocks-integration',
$style_url,
'',
$this->get_file_version( \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/index.css' ) ),
'all'
);
}
/**
@ -83,7 +74,6 @@ class WCS_Blocks_Integration implements IntegrationInterface {
return array(
'woocommerce-subscriptions-blocks' => 'active',
'place_order_override' => $this->get_place_order_button_text_override(),
'gifting_checkbox_text' => apply_filters( 'wcsg_enable_gifting_checkbox_label', get_option( WCSG_Admin::$option_prefix . '_gifting_checkbox_text', __( 'This is a gift', 'woocommerce-subscriptions' ) ) ),
);
}
@ -93,7 +83,7 @@ class WCS_Blocks_Integration implements IntegrationInterface {
* @param string $file Local path to the file.
* @return string The cache buster value to use for the given file.
*/
protected function get_file_version( $file ) {
public static function get_file_version( $file ) {
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $file ) ) {
return filemtime( $file );
}

View File

@ -237,7 +237,12 @@ class WCS_Cached_Data_Manager extends WCS_Cache_Manager {
protected function purge_subscription_user_cache( $subscription_id ) {
wcs_deprecated_argument( __METHOD__, '2.3.0', sprintf( __( 'Customer subscription caching is now handled by %1$s and %2$s.', 'woocommerce-subscriptions' ), 'WCS_Customer_Store_Cached_CPT', 'WCS_Post_Meta_Cache_Manager' ) ); // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment
$subscription = wcs_get_subscription( $subscription_id );
$subscription = wcs_get_subscription( $subscription_id );
if ( ! $subscription ) {
return;
}
$subscription_user_id = $subscription->get_user_id();
$this->log( sprintf(
'Clearing cache for user ID %1$s on %2$s hook.',

View File

@ -604,6 +604,10 @@ class WCS_Cart_Renewal {
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
if ( ! $subscription ) {
return $update_customer_data;
}
$billing_address = array();
if ( $checkout_object->checkout_fields['billing'] ) {
foreach ( array_keys( $checkout_object->checkout_fields['billing'] ) as $field ) {
@ -627,19 +631,6 @@ class WCS_Cart_Renewal {
return $update_customer_data;
}
/**
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
* @return bool
*/
public function is_purchasable( $is_purchasable, $product ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_renewal' );
return WCS_Limiter::is_purchasable_renewal( $is_purchasable, $product );
}
/**
* Flag payment of manual renewal orders via an extra URL param.
*
@ -1195,7 +1186,12 @@ class WCS_Cart_Renewal {
$cart_renewal_item = $this->cart_contains();
if ( false !== $cart_renewal_item ) {
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
if ( ! $subscription ) {
return;
}
$subscription_updated = false;
foreach ( [ 'billing', 'shipping' ] as $address_type ) {
@ -1231,6 +1227,10 @@ class WCS_Cart_Renewal {
if ( false !== $cart_renewal_item ) {
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
if ( ! $subscription ) {
return;
}
// Billing address is a required field.
foreach ( $request['billing_address'] as $key => $value ) {
if ( is_callable( [ $customer, "set_billing_$key" ] ) ) {

View File

@ -186,19 +186,6 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal {
return $cart_item_session_data;
}
/**
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
* @return bool
*/
public function is_purchasable( $is_purchasable, $product ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_renewal' );
return WCS_Limiter::is_purchasable_renewal( $is_purchasable, $product );
}
/**
* Checks the cart to see if it contains a subscription resubscribe item.
*

View File

@ -121,7 +121,6 @@ class WCS_Failed_Scheduled_Action_Manager {
// Log any exceptions caught by the exception listener in action logs.
if ( ! empty( $context['exceptions'] ) ) {
foreach ( $context['exceptions'] as $exception_message ) {
// @phpstan-ignore-next-line
ActionScheduler_Logger::instance()->log( $action_id, $exception_message );
}
}

View File

@ -23,8 +23,6 @@ class WCS_Limiter {
if ( wcs_is_frontend_request() || wcs_is_checkout_blocks_api_request() ) {
add_filter( 'woocommerce_subscription_is_purchasable', __CLASS__ . '::is_purchasable_switch', 12, 2 );
add_filter( 'woocommerce_subscription_variation_is_purchasable', __CLASS__ . '::is_purchasable_switch', 12, 2 );
add_filter( 'woocommerce_subscription_is_purchasable', __CLASS__ . '::is_purchasable_renewal', 12, 2 );
add_filter( 'woocommerce_subscription_variation_is_purchasable', __CLASS__ . '::is_purchasable_renewal', 12, 2 );
add_filter( 'woocommerce_valid_order_statuses_for_order_again', array( __CLASS__, 'filter_order_again_statuses_for_limited_subscriptions' ) );
}
}
@ -59,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.
*
@ -69,37 +104,66 @@ class WCS_Limiter {
* @return bool
*/
public static function is_purchasable( $purchasable, $product ) {
switch ( $product->get_type() ) {
case 'subscription':
case 'variable-subscription':
if ( true === $purchasable && false === self::is_purchasable_product( $purchasable, $product ) ) {
$purchasable = false;
// 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();
}
}
// 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;
}
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;
}
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;
}
}
}
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;
}
}
}
return $purchasable;
}
/**
* If a product is limited and the customer already has a subscription, mark it as not purchasable.
*
@ -107,6 +171,17 @@ class WCS_Limiter {
* @return bool
*/
public static function is_purchasable_product( $is_purchasable, $product ) {
_deprecated_function( __METHOD__, '8.0.0', 'WCS_Limiter::is_product_limited' );
return self::is_product_limited( $is_purchasable, $product );
}
/**
* If a product is limited and the customer already has a subscription, mark it as not purchasable.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.1, Moved from WC_Subscriptions_Product
* @return bool
*/
public static function is_product_limited( $is_purchasable, $product ) {
// Set up cache
if ( ! isset( self::$is_purchasable_cache[ $product->get_id() ] ) ) {
@ -197,33 +272,9 @@ class WCS_Limiter {
* @return bool
*/
public static function is_purchasable_renewal( $is_purchasable, $product ) {
if ( false === $is_purchasable && false === self::is_purchasable_product( $is_purchasable, $product ) ) {
$resubscribe_cart_item = wcs_cart_contains_resubscribe();
_deprecated_function( __METHOD__, '8.0.0', 'WCS_Limiter::is_product_limited' );
// Resubscribe logic
if ( isset( $_GET['resubscribe'] ) || false !== $resubscribe_cart_item ) {
$subscription_id = ( isset( $_GET['resubscribe'] ) ) ? absint( $_GET['resubscribe'] ) : $resubscribe_cart_item['subscription_resubscribe']['subscription_id'];
$subscription = wcs_get_subscription( $subscription_id );
if ( false != $subscription && $subscription->has_product( $product->get_id() ) && wcs_can_user_resubscribe_to( $subscription ) ) {
$is_purchasable = true;
}
// Renewal logic
} elseif ( isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() ) {
$is_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 ( $product->get_id() == $cart_item['product_id'] && ( isset( $cart_item['subscription_renewal'] ) || isset( $cart_item['subscription_resubscribe'] ) ) ) {
$is_purchasable = true;
break;
}
}
}
}
return $is_purchasable;
return ! self::is_product_limited( $is_purchasable, $product );
}
/**
@ -277,12 +328,73 @@ class WCS_Limiter {
return self::$order_awaiting_payment_for_product[ $product_id ];
}
/**
* Check if we're currently paying for a failed renewal order containing the product.
*
* @since 8.3.0 - Migrated from WooCommerce Subscriptions v2.1.0
* @param WC_Product $product The product to check.
* @return bool
*/
protected static function is_paying_for_failed_renewal_order( $product ) {
global $wp;
// Check if we're on the pay for order page
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['pay_for_order'] ) || ! isset( $_GET['key'] ) || ! isset( $wp->query_vars['order-pay'] ) ) {
// Also check if cart contains a failed renewal order payment
$failed_renewal_cart_item = wcs_cart_contains_failed_renewal_order_payment();
if ( false !== $failed_renewal_cart_item ) {
$cart_item_product_id = isset( $failed_renewal_cart_item['variation_id'] ) && $failed_renewal_cart_item['variation_id'] > 0
? $failed_renewal_cart_item['variation_id']
: $failed_renewal_cart_item['product_id'];
// Check both the product ID and parent product ID (for variations)
if ( (int) $product->get_id() === (int) $cart_item_product_id || (int) $product->get_id() === (int) $failed_renewal_cart_item['product_id'] ) {
return true;
}
// Also check if product is a variation and matches the parent
if ( $product->get_parent_id() > 0 && (int) $product->get_parent_id() === (int) $failed_renewal_cart_item['product_id'] ) {
return true;
}
}
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : '';
$order_id = isset( $wp->query_vars['order-pay'] ) ? $wp->query_vars['order-pay'] : 0;
$order = wc_get_order( absint( $order_id ) );
if ( ! $order || $order->get_order_key() !== $order_key ) {
return false;
}
// Check if order is a failed renewal order
if ( ! $order->has_status( 'failed' ) && ! wcs_order_contains_renewal( $order ) ) {
return false;
}
// Check if the order contains the product
foreach ( $order->get_items() as $item ) {
$item_product_id = isset( $item['variation_id'] ) && $item['variation_id'] > 0 ? $item['variation_id'] : $item['product_id'];
// Check both the product ID and parent product ID (for variations)
if ( (int) $product->get_id() === (int) $item_product_id || (int) $product->get_id() === (int) $item['product_id'] ) {
return true;
}
// Also check if product is a variation and matches the parent
if ( $product->get_parent_id() > 0 && (int) $product->get_parent_id() === (int) $item['product_id'] ) {
return true;
}
}
return false;
}
/**
* Filters the order statuses that enable the order again button and functionality.
*
* This function will return no statuses if the order contains non purchasable or limited products.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v3.0.2
* @since 8.3.0 - Migrated from WooCommerce Subscriptions v3.0.2
*
* @param array $statuses The order statuses that enable the order again button.
* @return array $statuses An empty array if the order contains limited products, otherwise the default statuses are returned.

View File

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

View File

@ -187,14 +187,20 @@ class WCS_My_Account_Payment_Methods {
return;
}
// translators: 1: token display name, 2: opening link tag, 4: closing link tag, 3: opening link tag.
$notice = sprintf( esc_html__( 'Would you like to update your subscriptions to use this new payment method - %1$s?%2$sYes%4$s | %3$sNo%4$s', 'woocommerce-subscriptions' ),
$notice = sprintf(
// translators: 1: token display name, 2: opening link tag, 4: closing link tag, 3: opening link tag.
esc_html__( 'Would you like to update your subscriptions to use this new payment method - %1$s?%2$sYes%4$s | %3$sNo%4$s', 'woocommerce-subscriptions' ),
$default_token->get_display_name(),
'</br><a href="' . esc_url( add_query_arg( array(
'update-subscription-tokens' => 'true',
'token-id' => $default_token_id,
'_wcsnonce' => wp_create_nonce( 'wcs-update-subscription-tokens' ),
), wc_get_account_endpoint_url( 'payment-methods' ) ) ) . '"><strong>',
'<br><a href="' . esc_url(
add_query_arg(
array(
'update-subscription-tokens' => 'true',
'token-id' => $default_token_id,
'_wcsnonce' => wp_create_nonce( 'wcs-update-subscription-tokens' ),
),
wc_get_account_endpoint_url( 'payment-methods' )
)
) . '"><strong>',
'<a href=""><strong>',
'</strong></a>'
);

View File

@ -92,11 +92,9 @@ class WCS_Object_Data_Cache_Manager extends WCS_Post_Meta_Cache_Manager {
}
$force_all_fields = 'all_fields' === $generate_type;
// @phpstan-ignore-next-line
$changes = $subscription->get_changes();
$base_data = $subscription->get_base_data();
// @phpstan-ignore-next-line
$meta_data = $subscription->get_meta_data();
$changes = $subscription->get_changes();
$base_data = $subscription->get_base_data();
$meta_data = $subscription->get_meta_data();
// Deleted meta won't be included in the changes, so we need to fetch the previous value via the raw meta data.
$data_store = $subscription->get_data_store();
@ -144,7 +142,6 @@ class WCS_Object_Data_Cache_Manager extends WCS_Post_Meta_Cache_Manager {
} elseif ( $meta->get_changes() ) {
// If the value is being updated.
$this->object_changes[ $subscription->get_id() ][ $data_key ] = [
// @phpstan-ignore-next-line
'new' => $meta->value,
'previous' => isset( $previous_meta['value'] ) ? $previous_meta['value'] : null,
'type' => 'update',
@ -152,7 +149,6 @@ class WCS_Object_Data_Cache_Manager extends WCS_Post_Meta_Cache_Manager {
} elseif ( $force_all_fields ) {
// If we're forcing all fields to be recorded.
$this->object_changes[ $subscription->get_id() ][ $data_key ] = [
// @phpstan-ignore-next-line
'new' => $meta->value,
'type' => 'add',
];

View File

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

View File

@ -280,7 +280,7 @@ class WCS_Staging {
if ( 'subscriptions_install' === $source ) {
$site_url = self::get_live_site_url();
} elseif ( ! is_multisite() && defined( 'WP_SITEURL' ) ) {
$site_url = WP_SITEURL;
$site_url = WP_SITEURL; // @phpstan-ignore phpstanWP.wpConstant.fetch (Using constant for performance and staging-specific behavior)
} else {
$site_url = get_site_url();
}

View File

@ -49,6 +49,10 @@ class WC_Subscriptions_Email_Preview {
case 'WCS_Email_Customer_Notification_Auto_Renewal':
$email->set_object( $this->get_dummy_subscription() );
break;
case 'WCSG_Email_Recipient_New_Initial_Order':
$email->set_object( $this->get_dummy_subscription() );
$email->subscriptions = [ $this->get_dummy_subscription() ];
break;
case 'WCS_Email_Customer_Payment_Retry':
case 'WCS_Email_Payment_Retry':
$email->retry = $this->get_dummy_retry( $email->object );
@ -191,8 +195,7 @@ class WC_Subscriptions_Email_Preview {
* @return bool Whether the email being previewed is a subscription email.
*/
private function is_subscription_email() {
return isset( WC_Subscriptions_Email::$email_classes[ $this->email_type ] )
|| isset( WC_Subscriptions_Email_Notifications::$email_classes[ $this->email_type ] )
return isset( apply_filters( 'wcs_email_classes', array_merge( WC_Subscriptions_Email::$email_classes, WC_Subscriptions_Email_Notifications::$email_classes ) )[ $this->email_type ] )
|| in_array( $this->email_type, [ 'WCS_Email_Customer_Payment_Retry', 'WCS_Email_Payment_Retry' ], true );
}

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.1
* @package WooCommerce_Subscriptions/Classes/Emails
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_Cancelled_Subscription extends WC_Email {

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.0.0
* @package WooCommerce/Classes/Emails
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Order {

View File

@ -12,7 +12,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.0.0
* @package WooCommerce/Classes/Emails
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order {

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @class WCS_Email_Customer_Notification_Auto_Renewal
* @version 1.0.0
* @package WooCommerce_Subscriptions/Classes/Emails
* @extends WC_Email
*/
class WCS_Email_Customer_Notification_Auto_Renewal extends WCS_Email_Customer_Notification {

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @class WCS_Email_Customer_Notification_Free_Trial_Expiry
* @version 1.0.0
* @package WooCommerce_Subscriptions/Classes/Emails
* @extends WC_Email
*/
class WCS_Email_Customer_Notification_Auto_Trial_Expiration extends WCS_Email_Customer_Notification {

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @class WCS_Email_Customer_Notification_Manual_Renewal
* @version 1.0.0
* @package WooCommerce_Subscriptions/Classes/Emails
* @extends WC_Email
*/
class WCS_Email_Customer_Notification_Manual_Renewal extends WCS_Email_Customer_Notification {

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @class WCS_Email_Customer_Notification_Free_Trial_Expiry
* @version 1.0.0
* @package WooCommerce_Subscriptions/Classes/Emails
* @extends WC_Email
*/
class WCS_Email_Customer_Notification_Manual_Trial_Expiration extends WCS_Email_Customer_Notification {

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @class WCS_Email_Customer_Notification_Subscription_Expiring
* @version 1.0.0
* @package WooCommerce_Subscriptions/Classes/Emails
* @extends WC_Email
*/
class WCS_Email_Customer_Notification_Subscription_Expiration extends WCS_Email_Customer_Notification {

View File

@ -10,7 +10,6 @@ defined( 'ABSPATH' ) || exit; // Exit if accessed directly.
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v3.0.0
* @package WooCommerce_Subscriptions/Includes/Emails
* @author WooCommerce.
* @extends WC_Email_Customer_On_Hold_Order
*/
class WCS_Email_Customer_On_Hold_Renewal_Order extends WC_Email_Customer_On_Hold_Order {
@ -22,8 +21,7 @@ class WCS_Email_Customer_On_Hold_Renewal_Order extends WC_Email_Customer_On_Hold
$this->customer_email = true;
$this->title = __( 'On-hold Renewal Order', 'woocommerce-subscriptions' );
$this->description = __( 'This is an order notification sent to customers containing order details after a renewal order is placed on-hold.', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {site_title} renewal order has been received!', 'woocommerce-subscriptions' );
$this->heading = __( 'Thank you for your renewal order', 'woocommerce-subscriptions' );
$this->template_html = 'emails/customer-on-hold-renewal-order.php';
$this->template_plain = 'emails/plain/customer-on-hold-renewal-order.php';
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
@ -48,7 +46,7 @@ class WCS_Email_Customer_On_Hold_Renewal_Order extends WC_Email_Customer_On_Hold
* @return string
*/
public function get_default_subject() {
return $this->subject;
return __( 'Your {site_title} renewal order has been received!', 'woocommerce-subscriptions' );
}
/**
@ -58,7 +56,7 @@ class WCS_Email_Customer_On_Hold_Renewal_Order extends WC_Email_Customer_On_Hold
* @return string
*/
public function get_default_heading() {
return $this->heading;
return __( 'Thank you for your renewal order', 'woocommerce-subscriptions' );
}
/**

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v1.4
* @package WooCommerce_Subscriptions/Includes/Emails
* @author Prospress
* @extends WC_Email_Customer_Invoice
*/
class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
@ -45,9 +44,6 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
$this->template_plain = 'emails/plain/customer-renewal-invoice.php';
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
$this->subject = __( 'Invoice for renewal order {order_number} from {order_date}', 'woocommerce-subscriptions' );
$this->heading = __( 'Invoice for renewal order {order_number}', 'woocommerce-subscriptions' );
// Triggers for this email
add_action( 'woocommerce_generated_manual_renewal_order_renewal_notification', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_failed_renewal_notification', array( $this, 'trigger' ) );
@ -64,7 +60,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
* @return string
*/
public function get_default_subject( $paid = false ) {
return $this->subject;
return __( 'Invoice for renewal order {order_number} from {order_date}', 'woocommerce-subscriptions' );
}
/**
@ -75,7 +71,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
* @return string
*/
public function get_default_heading( $paid = false ) {
return $this->heading;
return __( 'Invoice for renewal order {order_number}', 'woocommerce-subscriptions' );
}
/**
@ -128,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 );
}
/**
@ -138,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

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.1
* @package WooCommerce_Subscriptions/Classes/Emails
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_Expired_Subscription extends WC_Email {

View File

@ -9,7 +9,6 @@ if ( ! defined( 'ABSPATH' ) ) {
*
* @class WCS_Email_New_Renewal_Order
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v1.4
* @extends WC_Email_New_Order
*/
class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {

View File

@ -9,7 +9,6 @@ if ( ! defined( 'ABSPATH' ) ) {
*
* @class WCS_Email_New_Switch_Order
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v1.5
* @extends WC_Email_New_Order
*/
class WCS_Email_New_Switch_Order extends WC_Email_New_Order {

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.1
* @package WooCommerce_Subscriptions/Classes/Emails
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_On_Hold_Subscription extends WC_Email {

View File

@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.0.0
* @package WooCommerce/Classes/Emails
* @author Prospress
* @extends WC_Email
*/
class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Order {
@ -25,9 +24,6 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
$this->description = __( 'This is an order notification sent to the customer after payment for a subscription renewal order is completed. It contains the renewal order details.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'Thank you for your order', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {site_title} renewal order receipt from {order_date}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/customer-processing-renewal-order.php';
$this->template_plain = 'emails/plain/customer-processing-renewal-order.php';
$this->template_base = WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'templates/' );
@ -49,7 +45,7 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
* @return string
*/
public function get_default_subject() {
return $this->subject;
return __( 'Your {site_title} renewal order receipt from {order_date}', 'woocommerce-subscriptions' );
}
/**
@ -59,7 +55,7 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
* @return string
*/
public function get_default_heading() {
return $this->heading;
return __( 'Thank you for your order', 'woocommerce-subscriptions' );
}
/**

View File

@ -266,14 +266,14 @@ class WC_Subscriptions_Core_Payment_Gateways {
}
$status_html .= '<span class="payment-method-features-info tips" data-tip="';
$status_html .= esc_attr( '<strong><u>' . __( 'Supported features:', 'woocommerce-subscriptions' ) . '</u></strong></br>' . implode( '<br />', str_replace( '_', ' ', $core_features ) ) );
$status_html .= esc_attr( '<strong><u>' . __( 'Supported features:', 'woocommerce-subscriptions' ) . '</u></strong><br>' . implode( '<br>', str_replace( '_', ' ', $core_features ) ) );
if ( ! empty( $subscription_features ) ) {
$status_html .= esc_attr( '</br><strong><u>' . __( 'Subscription features:', 'woocommerce-subscriptions' ) . '</u></strong></br>' . implode( '<br />', str_replace( '_', ' ', $subscription_features ) ) );
$status_html .= esc_attr( '<br><strong><u>' . __( 'Subscription features:', 'woocommerce-subscriptions' ) . '</u></strong><br>' . implode( '<br>', str_replace( '_', ' ', $subscription_features ) ) );
}
if ( ! empty( $change_payment_method_features ) ) {
$status_html .= esc_attr( '</br><strong><u>' . __( 'Change payment features:', 'woocommerce-subscriptions' ) . '</u></strong></br>' . implode( '<br />', str_replace( '_', ' ', $change_payment_method_features ) ) );
$status_html .= esc_attr( '<br><strong><u>' . __( 'Change payment features:', 'woocommerce-subscriptions' ) . '</u></strong><br>' . implode( '<br>', str_replace( '_', ' ', $change_payment_method_features ) ) );
}
$status_html .= '"></span>';

View File

@ -299,7 +299,7 @@ abstract class WCS_SV_API_Base {
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v4.1.0
* @param string $uri current request URI
* @param \WCS_SV_API_Base $this class instance
* @param \WCS_SV_API_Base $instance class instance
*/
return apply_filters( 'wc_' . $this->get_api_id() . '_api_request_uri', $uri, $this );
}
@ -335,7 +335,7 @@ abstract class WCS_SV_API_Base {
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @param array $args request arguments
* @param \WCS_SV_API_Base $this class instance
* @param \WCS_SV_API_Base $instance class instance
*/
return apply_filters( 'wc_' . $this->get_api_id() . '_http_request_args', $args, $this );
}

View File

@ -590,7 +590,7 @@ class WCS_PayPal_Reference_Transaction_API_Request {
* Use this to modify the PayPal request parameters prior to validation
*
* @param array $parameters
* @param \WC_PayPal_Express_API_Request $this instance
* @param \WC_PayPal_Reference_Transaction_API_Request $instance instance
*/
$this->parameters = apply_filters( 'wcs_paypal_request_params', $this->parameters, $this );

View File

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

View File

@ -1,79 +0,0 @@
<?php
/**
* Simple Subscription Product Legacy Class
*
* Extends WC_Product_Subscription to provide compatibility methods when running WooCommerce < 3.0.
*
* @class WC_Product_Subscription_Legacy
* @package WooCommerce Subscriptions
* @category Class
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
*
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_Product_Subscription_Legacy extends WC_Product_Subscription {
var $subscription_price;
var $subscription_period;
var $subscription_period_interval;
var $subscription_length;
var $subscription_trial_length;
var $subscription_trial_period;
var $subscription_sign_up_fee;
/**
* Create a simple subscription product object.
*
* @access public
* @param mixed $product
*/
public function __construct( $product ) {
parent::__construct( $product );
$this->product_type = 'subscription';
// Load all meta fields
$this->product_custom_fields = get_post_meta( $this->id );
// Convert selected subscription meta fields for easy access
if ( ! empty( $this->product_custom_fields['_subscription_price'][0] ) ) {
$this->subscription_price = $this->product_custom_fields['_subscription_price'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_sign_up_fee'][0] ) ) {
$this->subscription_sign_up_fee = $this->product_custom_fields['_subscription_sign_up_fee'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_period'][0] ) ) {
$this->subscription_period = $this->product_custom_fields['_subscription_period'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_period_interval'][0] ) ) {
$this->subscription_period_interval = $this->product_custom_fields['_subscription_period_interval'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_length'][0] ) ) {
$this->subscription_length = $this->product_custom_fields['_subscription_length'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_trial_length'][0] ) ) {
$this->subscription_trial_length = $this->product_custom_fields['_subscription_trial_length'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_trial_period'][0] ) ) {
$this->subscription_trial_period = $this->product_custom_fields['_subscription_trial_period'][0];
}
$this->subscription_payment_sync_date = ( ! isset( $this->product_custom_fields['_subscription_payment_sync_date'][0] ) ) ? 0 : maybe_unserialize( $this->product_custom_fields['_subscription_payment_sync_date'][0] );
$this->subscription_one_time_shipping = ( ! isset( $this->product_custom_fields['_subscription_one_time_shipping'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_one_time_shipping'][0];
$this->subscription_limit = ( ! isset( $this->product_custom_fields['_subscription_limit'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_limit'][0];
}
}

View File

@ -1,101 +0,0 @@
<?php
/**
* Subscription Product Variation Legacy Class
*
* Extends WC_Product_Subscription_Variation to provide compatibility methods when running WooCommerce < 3.0.
*
* @class WC_Product_Subscription_Variation_Legacy
* @package WooCommerce Subscriptions
* @category Class
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
*
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_Product_Subscription_Variation_Legacy extends WC_Product_Subscription_Variation {
/**
* Set default array value for WC 3.0's data property.
* @var array
*/
protected $data = array();
/**
* Create a simple subscription product object.
*
* @access public
* @param mixed $product
*/
public function __construct( $product, $args = array() ) {
parent::__construct( $product, $args = array() );
$this->parent_product_type = $this->product_type;
$this->product_type = 'subscription_variation';
$this->subscription_variation_level_meta_data = array(
'subscription_price' => 0,
'subscription_period' => '',
'subscription_period_interval' => 'day',
'subscription_length' => 0,
'subscription_trial_length' => 0,
'subscription_trial_period' => 'day',
'subscription_sign_up_fee' => 0,
'subscription_payment_sync_date' => 0,
);
$this->variation_level_meta_data = array_merge( $this->variation_level_meta_data, $this->subscription_variation_level_meta_data );
}
/* Copied from WC 2.6 WC_Product_Variation */
/**
* __isset function.
*
* @param mixed $key
* @return bool
*/
public function __isset( $key ) {
if ( in_array( $key, array( 'variation_data', 'variation_has_stock' ) ) ) {
return true;
} elseif ( in_array( $key, array_keys( $this->variation_level_meta_data ) ) ) {
return metadata_exists( 'post', $this->variation_id, '_' . $key );
} elseif ( in_array( $key, array_keys( $this->variation_inherited_meta_data ) ) ) {
return metadata_exists( 'post', $this->variation_id, '_' . $key ) || metadata_exists( 'post', $this->id, '_' . $key );
} else {
return metadata_exists( 'post', $this->id, '_' . $key );
}
}
/**
* Get method returns variation meta data if set, otherwise in most cases the data from the parent.
*
* We need to use the WC_Product_Variation's __get() method, not the one in WC_Product_Subscription_Variation,
* which handles deprecation notices.
*
* @param string $key
* @return mixed
*/
public function __get( $key ) {
return WC_Product_Variation::__get( $key );
}
/**
* Provide a WC 3.0 method for variations.
*
* WC < 3.0 products have a get_parent() method, but this is not equivalent to the get_parent_id() method
* introduced in WC 3.0, because it derives the parent from $this->post->post_parent, but for variations,
* $this->post refers to the parent variable object's post, so $this->post->post_parent will be 0 under
* normal circumstances. Because of that, we can rely on wcs_get_objects_property( $this, 'parent_id' )
* and define this get_parent_id() method for variations even when WC 3.0 is not active.
*
* @param string $key
* @return mixed
*/
public function get_parent_id() {
return $this->id; // When WC < 3.0 is active, the ID property is the parent variable product's ID
}
}

View File

@ -1,441 +0,0 @@
<?php
/**
* Variable Subscription Product Legacy Class
*
* Extends WC_Product_Variable_Subscription to provide compatibility methods when running WooCommerce < 3.0.
*
* @class WC_Product_Variable_Subscription_Legacy
* @package WooCommerce Subscriptions
* @category Class
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
*
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_Product_Variable_Subscription_Legacy extends WC_Product_Variable_Subscription {
var $subscription_price;
var $subscription_period;
var $max_variation_period;
var $subscription_period_interval;
var $max_variation_period_interval;
var $product_type;
protected $prices_array;
/**
* Create a simple subscription product object.
*
* @access public
* @param mixed $product
*/
public function __construct( $product ) {
parent::__construct( $product );
$this->parent_product_type = $this->product_type;
$this->product_type = 'variable-subscription';
// Load all meta fields
$this->product_custom_fields = get_post_meta( $this->id );
// Convert selected subscription meta fields for easy access
if ( ! empty( $this->product_custom_fields['_subscription_price'][0] ) ) {
$this->subscription_price = $this->product_custom_fields['_subscription_price'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_sign_up_fee'][0] ) ) {
$this->subscription_sign_up_fee = $this->product_custom_fields['_subscription_sign_up_fee'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_period'][0] ) ) {
$this->subscription_period = $this->product_custom_fields['_subscription_period'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_period_interval'][0] ) ) {
$this->subscription_period_interval = $this->product_custom_fields['_subscription_period_interval'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_length'][0] ) ) {
$this->subscription_length = $this->product_custom_fields['_subscription_length'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_trial_length'][0] ) ) {
$this->subscription_trial_length = $this->product_custom_fields['_subscription_trial_length'][0];
}
if ( ! empty( $this->product_custom_fields['_subscription_trial_period'][0] ) ) {
$this->subscription_trial_period = $this->product_custom_fields['_subscription_trial_period'][0];
}
$this->subscription_payment_sync_date = 0;
$this->subscription_one_time_shipping = ( ! isset( $this->product_custom_fields['_subscription_one_time_shipping'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_one_time_shipping'][0];
$this->subscription_limit = ( ! isset( $this->product_custom_fields['_subscription_limit'][0] ) ) ? 'no' : $this->product_custom_fields['_subscription_limit'][0];
}
/**
* Get the min or max variation (active) price.
*
* This is a copy of WooCommerce < 2.4's get_variation_price() method, because 2.4.0 introduced a new
* transient caching system which assumes asort() on prices yields correct results for min/max prices
* (which it does for prices alone, but that's not the full story for subscription prices). Unfortunately,
* the new caching system is also hard to hook into so we'll just use the old system instead as the
* @see self::variable_product_sync() uses the old method also.
*
* @param string $min_or_max - min or max
* @param boolean $display Whether the value is going to be displayed
* @return string
*/
public function get_variation_price( $min_or_max = 'min', $display = false ) {
$variation_id = $this->get_meta( '_' . $min_or_max . '_price_variation_id', true );
if ( $display ) {
if ( $variation = wc_get_product( $variation_id ) ) {
if ( 'incl' == get_option( 'woocommerce_tax_display_shop' ) ) {
$price = wcs_get_price_including_tax( $variation );
} else {
$price = wcs_get_price_excluding_tax( $variation );
}
} else {
$price = '';
}
} else {
$price = $this->get_meta( '_price', true );
}
return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $display );
}
/**
* Get an array of all sale and regular prices from all variations re-ordered after WC has done a standard sort, to reflect subscription terms.
* The first and last element for each price type is the least and most expensive, respectively.
*
* @see WC_Product_Variable::get_variation_prices()
* @param bool $include_taxes Should taxes be included in the prices.
* @return array() Array of RAW prices, regular prices, and sale prices with keys set to variation ID.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
*/
public function get_variation_prices( $display = false ) {
$price_hash = $this->get_price_hash( $this, $display );
$this->prices_array[ $price_hash ] = parent::get_variation_prices( $display );
$children = array_keys( $this->prices_array[ $price_hash ]['price'] );
sort( $children );
$min_max_data = $this->get_min_and_max_variation_data( $children );
$min_variation_id = $min_max_data['min']['variation_id'];
$max_variation_id = $min_max_data['max']['variation_id'];
// Reorder the variable price arrays to reflect the min and max values so that WooCommerce will find them in the correct order
foreach ( $this->prices_array as $price_hash => $prices ) {
// Loop over sale_price, regular_price & price values to update them on main array
foreach ( $prices as $price_key => $variation_prices ) {
$min_price = $prices[ $price_key ][ $min_variation_id ];
$max_price = $prices[ $price_key ][ $max_variation_id ];
unset( $prices[ $price_key ][ $min_variation_id ] );
unset( $prices[ $price_key ][ $max_variation_id ] );
// append the minimum variation and prepend the maximum variation
$prices[ $price_key ] = array( $min_variation_id => $min_price ) + $prices[ $price_key ];
$prices[ $price_key ] += array( $max_variation_id => $max_price );
$this->prices_array[ $price_hash ][ $price_key ] = $prices[ $price_key ];
}
}
$this->subscription_price = $min_max_data['min']['price'];
$this->subscription_period = $min_max_data['min']['period'];
$this->subscription_period_interval = $min_max_data['min']['interval'];
$this->max_variation_price = $min_max_data['max']['price'];
$this->max_variation_period = $min_max_data['max']['period'];
$this->max_variation_period_interval = $min_max_data['max']['interval'];
$this->min_variation_price = $min_max_data['min']['price'];
$this->min_variation_regular_price = $min_max_data['min']['regular_price'];
return $this->prices_array[ $price_hash ];
}
/**
* Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters.
* DEVELOPERS should filter this hash if offering conditional pricing to keep it unique.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @param WC_Product
* @param bool $display Are prices for display? If so, taxes will be calculated.
* @return string
*/
protected function get_price_hash( $display = false ) {
global $wp_filter;
if ( $display ) {
$price_hash = array( get_option( 'woocommerce_tax_display_shop', 'excl' ), WC_Tax::get_rates() );
} else {
$price_hash = array( false );
}
$filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );
foreach ( $filter_names as $filter_name ) {
if ( ! empty( $wp_filter[ $filter_name ] ) ) {
$price_hash[ $filter_name ] = array();
foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) {
$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
}
}
}
$price_hash = md5( json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $this, $display ) ) );
return $price_hash;
}
/**
* Sync variable product prices with the children lowest/highest prices.
*
* @param int $product_id The ID of the product.
* @return void
*/
public function variable_product_sync( $product_id = 0 ) {
WC_Product_Variable::variable_product_sync( $product_id );
$child_variation_ids = $this->get_children( true );
if ( $child_variation_ids ) {
$min_max_data = wcs_get_min_max_variation_data( $this, $child_variation_ids );
$this->set_min_and_max_variation_data( $min_max_data, $child_variation_ids );
update_post_meta( $this->id, '_min_price_variation_id', $min_max_data['min']['variation_id'] );
update_post_meta( $this->id, '_max_price_variation_id', $min_max_data['max']['variation_id'] );
update_post_meta( $this->id, '_price', $min_max_data['min']['price'] );
update_post_meta( $this->id, '_min_variation_price', $min_max_data['min']['price'] );
update_post_meta( $this->id, '_max_variation_price', $min_max_data['max']['price'] );
update_post_meta( $this->id, '_min_variation_regular_price', $min_max_data['min']['regular_price'] );
update_post_meta( $this->id, '_max_variation_regular_price', $min_max_data['max']['regular_price'] );
update_post_meta( $this->id, '_min_variation_sale_price', $min_max_data['min']['sale_price'] );
update_post_meta( $this->id, '_max_variation_sale_price', $min_max_data['max']['sale_price'] );
update_post_meta( $this->id, '_min_variation_period', $min_max_data['min']['period'] );
update_post_meta( $this->id, '_max_variation_period', $min_max_data['max']['period'] );
update_post_meta( $this->id, '_min_variation_period_interval', $min_max_data['min']['interval'] );
update_post_meta( $this->id, '_max_variation_period_interval', $min_max_data['max']['interval'] );
update_post_meta( $this->id, '_subscription_price', $min_max_data['min']['price'] );
update_post_meta( $this->id, '_subscription_sign_up_fee', $min_max_data['subscription']['signup-fee'] );
update_post_meta( $this->id, '_subscription_period', $min_max_data['min']['period'] );
update_post_meta( $this->id, '_subscription_period_interval', $min_max_data['min']['interval'] );
update_post_meta( $this->id, '_subscription_trial_period', $min_max_data['subscription']['trial_period'] );
update_post_meta( $this->id, '_subscription_trial_length', $min_max_data['subscription']['trial_length'] );
update_post_meta( $this->id, '_subscription_length', $min_max_data['subscription']['length'] );
$this->subscription_price = $min_max_data['min']['price'];
$this->subscription_period = $min_max_data['min']['period'];
$this->subscription_period_interval = $min_max_data['min']['interval'];
$this->subscription_sign_up_fee = $min_max_data['subscription']['signup-fee'];
$this->subscription_trial_period = $min_max_data['subscription']['trial_period'];
$this->subscription_trial_length = $min_max_data['subscription']['trial_length'];
$this->subscription_length = $min_max_data['subscription']['length'];
if ( function_exists( 'wc_delete_product_transients' ) ) {
wc_delete_product_transients( $this->id );
} else {
WC()->clear_product_transients( $this->id );
}
} else { // No variations yet
$this->subscription_price = '';
$this->subscription_sign_up_fee = '';
$this->subscription_period = 'day';
$this->subscription_period_interval = 1;
$this->subscription_trial_period = 'day';
$this->subscription_trial_length = 1;
$this->subscription_length = 0;
}
}
/**
* Returns the price in html format.
*
* @access public
* @param string $price (default: '')
* @return string
*/
public function get_price_html( $price = '' ) {
if ( ! isset( $this->subscription_period ) || ! isset( $this->subscription_period_interval ) || ! isset( $this->max_variation_period ) || ! isset( $this->max_variation_period_interval ) ) {
$this->variable_product_sync();
}
// Only create the subscription price string when a price has been set
if ( $this->subscription_price !== '' ) {
$price = '';
if ( $this->is_on_sale() && isset( $this->min_variation_price ) && $this->min_variation_regular_price !== $this->get_price() ) {
if ( ! $this->min_variation_price || $this->min_variation_price !== $this->max_variation_price ) {
$price .= wcs_get_price_html_from_text( $this );
}
$variation_id = $this->get_meta( '_min_price_variation_id', true );
$variation = wc_get_product( $variation_id );
$tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
$sale_price_args = array(
'qty' => 1,
'price' => $variation->get_sale_price(),
);
$regular_price_args = array(
'qty' => 1,
'price' => $variation->get_regular_price(),
);
if ( 'incl' == $tax_display_mode ) {
$sale_price = wcs_get_price_including_tax( $variation, $sale_price_args );
$regular_price = wcs_get_price_including_tax( $variation, $regular_price_args );
} else {
$sale_price = wcs_get_price_excluding_tax( $variation, $sale_price_args );
$regular_price = wcs_get_price_excluding_tax( $variation, $regular_price_args );
}
$price .= $this->get_price_html_from_to( $regular_price, $sale_price );
} else {
if ( $this->min_variation_price !== $this->max_variation_price ) {
$price .= wcs_get_price_html_from_text( $this );
}
$price .= wc_price( $this->get_variation_price( 'min', true ) );
}
// Make sure the price contains "From:" when billing schedule differs between variations
if ( false === strpos( $price, wcs_get_price_html_from_text( $this ) ) ) {
if ( $this->subscription_period !== $this->max_variation_period ) {
$price = wcs_get_price_html_from_text( $this ) . $price;
} elseif ( $this->subscription_period_interval !== $this->max_variation_period_interval ) {
$price = wcs_get_price_html_from_text( $this ) . $price;
}
}
$price .= $this->get_price_suffix();
$price = WC_Subscriptions_Product::get_price_string( $this, array( 'price' => $price ) );
}
return apply_filters( 'woocommerce_variable_subscription_price_html', $price, $this );
}
/**
* Provide the WC_Data::get_meta() function when WC < 3.0 is active.
*
* @param string $meta_key
* @param bool $single
* @param string $context
* @return object WC_Product_Subscription or WC_Product_Subscription_Variation
*/
function get_meta( $meta_key = '', $single = true, $context = 'view' ) {
return $this->get_meta( $meta_key, $single );
}
/**
* get_child function.
*
* @access public
* @param mixed $child_id
* @return object WC_Product_Subscription or WC_Product_Subscription_Variation
*/
public function get_child( $child_id ) {
return wc_get_product( $child_id, array(
'product_type' => 'Subscription_Variation',
'parent_id' => $this->id,
'parent' => $this,
) );
}
/**
* Get default attributes.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @param string $context
* @return array
*/
public function get_default_attributes( $context = 'view' ) {
return $this->get_variation_default_attributes();
}
/**
* Set the product's min and max variation data.
*
* @param array $min_and_max_data The min and max variation data returned by @see wcs_get_min_max_variation_data(). Optional.
* @param array $variation_ids The visible child variation IDs. Optional. By default this value be generated by @see WC_Product_Variable->get_children( true ).
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.3.0
*/
public function set_min_and_max_variation_data( $min_and_max_data = array(), $variation_ids = array() ) {
if ( empty( $variation_ids ) ) {
$variation_ids = $this->get_children( true );
}
if ( empty( $min_and_max_data ) ) {
$min_and_max_data = wcs_get_min_max_variation_data( $this, $variation_ids );
}
update_post_meta( $this->id, '_min_max_variation_data', $min_and_max_data, true );
update_post_meta( $this->id, '_min_max_variation_ids_hash', $this->get_variation_ids_hash( $variation_ids ), true );
}
/**
* Get the min and max variation data.
*
* This is a wrapper for @see wcs_get_min_max_variation_data() but to avoid calling
* that resource intensive function multiple times per request, check the value
* stored in meta or cached in memory before calling that function.
*
* @param array $variation_ids An array of variation IDs.
* @return array The variable product's min and max variation data.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.3.0
*/
public function get_min_and_max_variation_data( $variation_ids ) {
$variation_ids_hash = $this->get_variation_ids_hash( $variation_ids );
// If this variable product has no min and max variation data, set it.
if ( ! metadata_exists( 'post', $this->id, '_min_max_variation_ids_hash' ) ) {
$this->set_min_and_max_variation_data();
}
if ( $variation_ids_hash === $this->get_meta( '_min_max_variation_ids_hash', true ) ) {
$min_and_max_variation_data = $this->get_meta( '_min_max_variation_data', true );
} elseif ( ! empty( $this->min_max_variation_data[ $variation_ids_hash ] ) ) {
$min_and_max_variation_data = $this->min_max_variation_data[ $variation_ids_hash ];
} else {
$min_and_max_variation_data = wcs_get_min_max_variation_data( $this, $variation_ids );
$this->min_max_variation_data[ $variation_ids_hash ] = $min_and_max_variation_data;
}
return $min_and_max_variation_data;
}
}

View File

@ -1,776 +0,0 @@
<?php
/**
* Subscription Legacy Object
*
* Extends WC_Subscription to provide WC 3.0 methods when running WooCommerce < 3.0.
*
* @class WC_Subscription_Legacy
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.1
* @package WooCommerce Subscriptions/Classes
* @category Class
* @author Brent Shepherd
*/
class WC_Subscription_Legacy extends WC_Subscription {
protected $schedule;
protected $status_transition = false;
/**
* Whether the object has been read. Pre WC 3.0 subscription objects are always read by default.
* Provides an accessible variable equivalent to WC_Data::$object_read pre WC 3.0.
*
* @protected boolean
*/
protected $object_read = true;
/**
* Initialize the subscription object.
*
* @param int|WC_Subscription $subscription
*/
public function __construct( $subscription ) {
parent::__construct( $subscription );
$this->order_type = 'shop_subscription';
$this->schedule = new stdClass();
}
/**
* Populates a subscription from the loaded post data.
*
* @param mixed $result
*/
public function populate( $result ) {
parent::populate( $result );
if ( $this->post->post_parent > 0 ) {
$this->order = wc_get_order( $this->post->post_parent );
}
}
/**
* Returns the unique ID for this object.
*
* @return int
*/
public function get_id() {
return $this->id;
}
/**
* Get parent order ID.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @return int
*/
public function get_parent_id() {
return $this->post->post_parent;
}
/**
* Gets order currency.
*
* @return string
*/
public function get_currency() {
return $this->get_order_currency();
}
/**
* Get customer_note.
*
* @param string $context
* @return string
*/
public function get_customer_note( $context = 'view' ) {
return $this->customer_note;
}
/**
* Get prices_include_tax.
*
* @param string $context
* @return bool
*/
public function get_prices_include_tax( $context = 'view' ) {
return $this->prices_include_tax;
}
/**
* Get the payment method.
*
* @param string $context
* @return string
*/
public function get_payment_method( $context = 'view' ) {
return $this->payment_method;
}
/**
* Get the payment method's title.
*
* @param string $context
* @return string
*/
public function get_payment_method_title( $context = 'view' ) {
return $this->payment_method_title;
}
/** Address Getters **/
/**
* Get billing_first_name.
*
* @param string $context
* @return string
*/
public function get_billing_first_name( $context = 'view' ) {
return $this->billing_first_name;
}
/**
* Get billing_last_name.
*
* @param string $context
* @return string
*/
public function get_billing_last_name( $context = 'view' ) {
return $this->billing_last_name;
}
/**
* Get billing_company.
*
* @param string $context
* @return string
*/
public function get_billing_company( $context = 'view' ) {
return $this->billing_company;
}
/**
* Get billing_address_1.
*
* @param string $context
* @return string
*/
public function get_billing_address_1( $context = 'view' ) {
return $this->billing_address_1;
}
/**
* Get billing_address_2.
*
* @param string $context
* @return string $value
*/
public function get_billing_address_2( $context = 'view' ) {
return $this->billing_address_2;
}
/**
* Get billing_city.
*
* @param string $context
* @return string $value
*/
public function get_billing_city( $context = 'view' ) {
return $this->billing_city;
}
/**
* Get billing_state.
*
* @param string $context
* @return string
*/
public function get_billing_state( $context = 'view' ) {
return $this->billing_state;
}
/**
* Get billing_postcode.
*
* @param string $context
* @return string
*/
public function get_billing_postcode( $context = 'view' ) {
return $this->billing_postcode;
}
/**
* Get billing_country.
*
* @param string $context
* @return string
*/
public function get_billing_country( $context = 'view' ) {
return $this->billing_country;
}
/**
* Get billing_email.
*
* @param string $context
* @return string
*/
public function get_billing_email( $context = 'view' ) {
return $this->billing_email;
}
/**
* Get billing_phone.
*
* @param string $context
* @return string
*/
public function get_billing_phone( $context = 'view' ) {
return $this->billing_phone;
}
/**
* Get shipping_first_name.
*
* @param string $context
* @return string
*/
public function get_shipping_first_name( $context = 'view' ) {
return $this->shipping_first_name;
}
/**
* Get shipping_last_name.
*
* @param string $context
* @return string
*/
public function get_shipping_last_name( $context = 'view' ) {
return $this->shipping_last_name;
}
/**
* Get shipping_company.
*
* @param string $context
* @return string
*/
public function get_shipping_company( $context = 'view' ) {
return $this->shipping_company;
}
/**
* Get shipping_address_1.
*
* @param string $context
* @return string
*/
public function get_shipping_address_1( $context = 'view' ) {
return $this->shipping_address_1;
}
/**
* Get shipping_address_2.
*
* @param string $context
* @return string
*/
public function get_shipping_address_2( $context = 'view' ) {
return $this->shipping_address_2;
}
/**
* Get shipping_city.
*
* @param string $context
* @return string
*/
public function get_shipping_city( $context = 'view' ) {
return $this->shipping_city;
}
/**
* Get shipping_state.
*
* @param string $context
* @return string
*/
public function get_shipping_state( $context = 'view' ) {
return $this->shipping_state;
}
/**
* Get shipping_postcode.
*
* @param string $context
* @return string
*/
public function get_shipping_postcode( $context = 'view' ) {
return $this->shipping_postcode;
}
/**
* Get shipping_country.
*
* @param string $context
* @return string
*/
public function get_shipping_country( $context = 'view' ) {
return $this->shipping_country;
}
/**
* Get order key.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @param string $context
* @return string
*/
public function get_order_key( $context = 'view' ) {
return $this->order_key;
}
/**
* Get date_created.
*
* Used by parent::get_date()
*
* @throws WC_Data_Exception
* @return DateTime|NULL object if the date is set or null if there is no date.
*/
public function get_date_created( $context = 'view' ) {
if ( '0000-00-00 00:00:00' != $this->post->post_date_gmt ) {
$datetime = new WC_DateTime( $this->post->post_date_gmt, new DateTimeZone( 'UTC' ) );
$datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
} else {
$datetime = new WC_DateTime( $this->post->post_date, new DateTimeZone( wc_timezone_string() ) );
}
// Cache it in $this->schedule for backward compatibility
if ( ! isset( $this->schedule->start ) ) {
$this->schedule->start = wcs_get_datetime_utc_string( $datetime );
}
return $datetime;
}
/**
* Get date_modified.
*
* Used by parent::get_date()
*
* @throws WC_Data_Exception
* @return DateTime|NULL object if the date is set or null if there is no date.
*/
public function get_date_modified( $context = 'view' ) {
if ( '0000-00-00 00:00:00' != $this->post->post_modified_gmt ) {
$datetime = new WC_DateTime( $this->post->post_modified_gmt, new DateTimeZone( 'UTC' ) );
$datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
} else {
$datetime = new WC_DateTime( $this->post->post_modified, new DateTimeZone( wc_timezone_string() ) );
}
return $datetime;
}
/**
* Check if a given line item on the subscription had a sign-up fee, and if so, return the value of the sign-up fee.
*
* The single quantity sign-up fee will be returned instead of the total sign-up fee paid. For example, if 3 x a product
* with a 10 BTC sign-up fee was purchased, a total 30 BTC was paid as the sign-up fee but this function will return 10 BTC.
*
* @param array|int Either an order item (in the array format returned by self::get_items()) or the ID of an order item.
* @param string $tax_inclusive_or_exclusive Whether or not to adjust sign up fee if prices inc tax - ensures that the sign up fee paid amount includes the paid tax if inc
* @return bool
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
*/
public function get_items_sign_up_fee( $line_item, $tax_inclusive_or_exclusive = 'exclusive_of_tax' ) {
if ( ! is_array( $line_item ) ) {
$line_item = wcs_get_order_item( $line_item, $this );
}
$parent_order = $this->get_parent();
// If there was no original order, nothing was paid up-front which means no sign-up fee
if ( false == $parent_order ) {
$sign_up_fee = 0;
} else {
$original_order_item = '';
// Find the matching item on the order
foreach ( $parent_order->get_items() as $order_item ) {
if ( wcs_get_canonical_product_id( $line_item ) == wcs_get_canonical_product_id( $order_item ) ) {
$original_order_item = $order_item;
break;
}
}
// No matching order item, so this item wasn't purchased in the original order
if ( empty( $original_order_item ) ) {
$sign_up_fee = 0;
} elseif ( isset( $line_item['item_meta']['_has_trial'] ) ) {
// Sign up is total amount paid for this item on original order when item has a free trial
$sign_up_fee = $original_order_item['line_total'] / $original_order_item['qty'];
} elseif ( isset( $original_order_item['item_meta']['_synced_sign_up_fee'] ) ) {
$sign_up_fee = $original_order_item['item_meta']['_synced_sign_up_fee'] / $original_order_item['qty'];
// The synced sign up fee meta contains the raw product sign up fee, if the subscription totals are inclusive of tax, we need to adjust the synced sign up fee to match tax inclusivity.
if ( $this->get_prices_include_tax() ) {
$line_item_total = $original_order_item['line_total'] + $original_order_item['line_tax'];
$signup_fee_portion = $sign_up_fee / $line_item_total;
$sign_up_fee = $original_order_item['line_total'] * $signup_fee_portion;
}
} else {
// Sign-up fee is any amount on top of recurring amount
$sign_up_fee = max( $original_order_item['line_total'] / $original_order_item['qty'] - $line_item['line_total'] / $line_item['qty'], 0 );
}
// If prices don't inc tax, ensure that the sign up fee amount includes the tax.
if ( 'inclusive_of_tax' === $tax_inclusive_or_exclusive && ! empty( $original_order_item ) && ! empty( $sign_up_fee ) ) {
$sign_up_fee_proportion = $sign_up_fee / ( $original_order_item['line_total'] / $original_order_item['qty'] );
$sign_up_fee_tax = $original_order_item['line_tax'] * $sign_up_fee_proportion;
$sign_up_fee += $sign_up_fee_tax;
$sign_up_fee = wc_format_decimal( $sign_up_fee, wc_get_price_decimals() );
}
}
return apply_filters( 'woocommerce_subscription_items_sign_up_fee', $sign_up_fee, $line_item, $this, $tax_inclusive_or_exclusive );
}
/**
* Helper function to make sure when WC_Subscription calls get_prop() from
* it's new getters that the property is both retrieved from the legacy class
* property and done so from post meta.
*
* For inherited dates props, like date_created, date_modified, date_paid,
* date_completed, we want to use our own get_date() function rather simply
* getting the stored value. Otherwise, we either get the prop set in memory
* or post meta if it's not set yet, because __get() in WC < 3.0 would fallback
* to post meta.
*
* @param string
* @param string
* @return mixed
*/
protected function get_prop( $prop, $context = 'view' ) {
if ( 'switch_data' == $prop ) {
$prop = 'subscription_switch_data';
}
// The requires manual renewal prop uses boolean values but is stored as a string so needs special handling, it also needs to be handled before the checks on $this->$prop to avoid triggering __isset() & __get() magic methods for $this->requires_manual_renewal
if ( 'requires_manual_renewal' === $prop ) {
$value = $this->get_meta( '_' . $prop, true );
if ( 'false' === $value || '' === $value ) {
$value = false;
} else {
$value = true;
}
} elseif ( ! isset( $this->$prop ) || empty( $this->$prop ) ) {
$value = $this->get_meta( '_' . $prop, true );
} else {
$value = $this->$prop;
}
return $value;
}
/**
* Get the stored date for a specific schedule.
*
* @param string $date_type 'date_created', 'trial_end', 'next_payment', 'last_order_date_created' or 'end'
*/
protected function get_date_prop( $date_type ) {
$datetime = parent::get_date_prop( $date_type );
// Cache the string equalivent of it in $this->schedule for backward compatibility
if ( ! isset( $this->schedule->{$date_type} ) ) {
if ( ! is_object( $datetime ) ) {
$this->schedule->{$date_type} = 0;
} else {
$this->schedule->{$date_type} = wcs_get_datetime_utc_string( $datetime );
}
}
return wcs_get_datetime_from( wcs_date_to_time( $datetime ) );
}
/*** Setters *****************************************************/
/**
* Set the unique ID for this object.
*
* @param int
*/
public function set_id( $id ) {
$this->id = absint( $id );
}
/**
* Set parent order ID. We don't use WC_Abstract_Order::set_parent_id() because we want to allow false
* parent IDs, like 0.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @param int $value
*/
public function set_parent_id( $value ) {
// Update the parent in the database
wp_update_post( array(
'ID' => $this->id,
'post_parent' => $value,
) );
// And update the parent in memory
$this->post->post_parent = $value;
$this->order = null;
}
/**
* Set subscription status.
*
* @param string $new_status Status to change the order to. No internal wc- prefix is required.
* @return array details of change
*/
public function set_status( $new_status, $note = '', $manual_update = false ) {
$old_status = $this->get_status();
$prefix = substr( $new_status, 0, 3 );
$new_status = 'wc-' === $prefix ? substr( $new_status, 3 ) : $new_status;
wp_update_post(
array(
'ID' => $this->get_id(),
'post_status' => wcs_maybe_prefix_key( $new_status, 'wc-' ),
)
);
$this->post_status = $this->post->post_status = wcs_maybe_prefix_key( $new_status, 'wc-' );
if ( $old_status !== $new_status ) {
$this->status_transition = array(
'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $old_status,
'to' => $new_status,
'note' => $note,
'manual' => (bool) $manual_update,
);
}
return array(
'from' => $old_status,
'to' => $new_status,
);
}
/**
* Helper function to make sure when WC_Subscription calls set_prop() that property is
* both set in the legacy class property and saved in post meta immediately.
*
* @param string $prop
* @param mixed $value
*/
protected function set_prop( $prop, $value ) {
if ( 'switch_data' == $prop ) {
$prop = 'subscription_switch_data';
}
$this->$prop = $value;
// The requires manual renewal prop uses boolean values but it stored as a string
if ( 'requires_manual_renewal' === $prop ) {
if ( false === $value || '' === $value ) {
$value = 'false';
} else {
$value = 'true';
}
}
update_post_meta( $this->get_id(), '_' . $prop, $value );
}
/**
* Set the stored date for a specific schedule.
*
* @param string $date_type 'trial_end', 'next_payment', 'cancelled', 'payment_retry' or 'end'
* @param int $value UTC timestamp
*/
protected function set_date_prop( $date_type, $value ) {
$datetime = wcs_get_datetime_from( $value );
$date = ! is_null( $datetime ) ? wcs_get_datetime_utc_string( $datetime ) : 0;
$this->set_prop( $this->get_date_prop_key( $date_type ), $date );
$this->schedule->{$date_type} = $date;
}
/**
* Set a certain date type for the last order on the subscription.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @param string $date_type
* @param string|integer|object
* @return WC_DateTime|NULL object if the date is set or null if there is no date.
*/
protected function set_last_order_date( $date_type, $date = null ) {
$last_order = $this->get_last_order( 'all' );
if ( $last_order ) {
$datetime = wcs_get_datetime_from( $date );
switch ( $date_type ) {
case 'date_paid':
update_post_meta( $last_order->id, '_paid_date', ! is_null( $date ) ? $datetime->date( 'Y-m-d H:i:s' ) : '' );
// Preemptively set the UTC timestamp for WC 3.0+ also to avoid incorrect values when the site's timezone is changed between now and upgrading to WC 3.0
update_post_meta( $last_order->id, '_date_paid', ! is_null( $date ) ? $datetime->getTimestamp() : '' );
break;
case 'date_completed':
update_post_meta( $last_order->id, '_completed_date', ! is_null( $date ) ? $datetime->date( 'Y-m-d H:i:s' ) : '' );
// Preemptively set the UTC timestamp for WC 3.0+ also to avoid incorrect values when the site's timezone is changed between now and upgrading to WC 3.0
update_post_meta( $last_order->id, '_date_completed', ! is_null( $date ) ? $datetime->getTimestamp() : '' );
break;
case 'date_modified':
wp_update_post( array(
'ID' => $last_order->id,
'post_modified' => $datetime->date( 'Y-m-d H:i:s' ),
'post_modified_gmt' => wcs_get_datetime_utc_string( $datetime ),
) );
break;
case 'date_created':
wp_update_post( array(
'ID' => $last_order->id,
'post_date' => $datetime->date( 'Y-m-d H:i:s' ),
'post_date_gmt' => wcs_get_datetime_utc_string( $datetime ),
) );
break;
}
}
}
/**
* Set date_created.
*
* Used by parent::update_dates()
*
* @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
* @throws WC_Data_Exception
*/
public function set_date_created( $date = null ) {
global $wpdb;
if ( ! is_null( $date ) ) {
$datetime_string = wcs_get_datetime_utc_string( wcs_get_datetime_from( $date ) );
// Don't use wp_update_post() to avoid infinite loops here
$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_date = %s, post_date_gmt = %s WHERE ID = %d", get_date_from_gmt( $datetime_string ), $datetime_string, $this->get_id() ) );
$this->post->post_date = get_date_from_gmt( $datetime_string );
$this->post->post_date_gmt = $datetime_string;
}
}
/**
* Set discount_total.
*
* @param string $value
* @throws WC_Data_Exception
*/
public function set_discount_total( $value ) {
$this->set_total( $value, 'cart_discount' );
}
/**
* Set discount_tax.
*
* @param string $value
* @throws WC_Data_Exception
*/
public function set_discount_tax( $value ) {
$this->set_total( $value, 'cart_discount_tax' );
}
/**
* Set shipping_total.
*
* @param string $value
* @throws WC_Data_Exception
*/
public function set_shipping_total( $value ) {
$this->set_total( $value, 'shipping' );
}
/**
* Set shipping_tax.
*
* @param string $value
* @throws WC_Data_Exception
*/
public function set_shipping_tax( $value ) {
$this->set_total( $value, 'shipping_tax' );
}
/**
* Set cart tax.
*
* @param string $value
* @throws WC_Data_Exception
*/
public function set_cart_tax( $value ) {
$this->set_total( $value, 'tax' );
}
/**
* Save data to the database. Nothing to do here as it's all done separately when calling @see this->set_prop().
*
* @return int order ID
*/
public function save() {
$this->status_transition();
return $this->get_id();
}
/**
* Update meta data by key or ID, if provided.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
* @param string $key
* @param string $value
* @param int $meta_id
*/
public function update_meta_data( $key, $value, $meta_id = '' ) {
if ( ! empty( $meta_id ) ) {
update_metadata_by_mid( 'post', $meta_id, $value, $key );
} else {
update_post_meta( $this->get_id(), $key, $value );
}
}
/**
* Save subscription date changes to the database.
* Nothing to do here as all date properties are saved when calling @see $this->set_prop().
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.6
*/
public function save_dates() {
// Nothing to do here.
}
}

View File

@ -1,89 +0,0 @@
<?php
/**
* There is "magic" in PHP, and then there is this.
*
* Story time: once upon a time, in a land not too far away, WooCommerce 3.0 deprecated accessing
* all properties on objects. A conventicle of wizards known as __get(), __set() and __isset() came
* together to make sure that properties on Subscriptions products could still be used, despite not being
* accessible. However, a dark cloud hung over properties which were arrays. None of the conventicle
* new of magic powerful enough to deal with such a problem. Enter Cesar, who summoned the dark arts
* to call upon the ArrayAccess incantation.
*
* In other words, this class is used to access specific items on an array from within the magic methods
* of other objects, like WC_Product_Subscription_Variation::__get() for the property formerly known as
* $subscription_variation_level_meta_data.
*
* @package WooCommerce Subscriptions
* @category Class
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
*
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Array_Property_Post_Meta_Black_Magic implements ArrayAccess {
/**
* Store the ID this class is being used against so that we use it for post meta calls.
*/
protected $product_id;
/**
* Constructor
*
* @access public
* @param mixed $product
*/
public function __construct( $product_id ) {
$this->product_id = $product_id;
}
/**
* offsetGet
* @param string $key
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet( $key ) {
return get_post_meta( $this->product_id, $this->maybe_prefix_meta_key( $key ), true );
}
/**
* offsetSet
* @param string $key
* @param mixed $value
*/
#[\ReturnTypeWillChange]
public function offsetSet( $key, $value ) {
update_post_meta( $this->product_id, $this->maybe_prefix_meta_key( $key ), $value );
}
/**
* offsetExists
* @param string $key
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists( $key ) {
return metadata_exists( 'post', $this->product_id, $this->maybe_prefix_meta_key( $key ) );
}
/**
* Nothing to do here as we access post meta directly.
*/
#[\ReturnTypeWillChange]
public function offsetUnset( $key ) {
}
/**
* We only work with post meta data that has meta keys prefixed with an underscore, so
* add a prefix if it is not already set.
*/
protected function maybe_prefix_meta_key( $key ) {
if ( '_' != substr( $key, 0, 1 ) ) {
$key = '_' . $key;
}
return $key;
}
}

View File

@ -1,41 +0,0 @@
<?php
/**
* Legacy Subscription Product Handler
*
* Ensures subscription products work with versions of WooCommerce prior to 3.0 by loading
* legacy classes to provide CRUD methods only added with WC 3.0.
*
* @package WooCommerce Subscriptions
* @subpackage WCS_Product_Legacy
* @category Class
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
*/
class WCS_Product_Legacy {
/**
* Set up the class, including it's hooks & filters, when the file is loaded.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
**/
public static function init() {
// Use our legacy product classes when WC 3.0+ is not active
add_filter( 'woocommerce_product_class', __CLASS__ . '::set_product_class', 100, 4 );
}
/**
* Use legacy classes for WC < 3.0
*
* @return string $classname The name of the WC_Product_* class which should be instantiated to create an instance of this product.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.0
*/
public static function set_product_class( $classname, $product_type, $post_type, $product_id ) {
if ( wcs_is_woocommerce_pre( '3.0' ) && in_array( $classname, array( 'WC_Product_Subscription', 'WC_Product_Variable_Subscription', 'WC_Product_Subscription_Variation' ) ) ) {
$classname .= '_Legacy';
}
return $classname;
}
}

View File

@ -226,6 +226,10 @@ class WC_Subscriptions_Upgrader {
if ( version_compare( self::$stored_plugin_version, '7.8.0', '<' ) ) {
WCS_Plugin_Upgrade_7_8_0::check_gifting_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

@ -0,0 +1,40 @@
<?php
/**
* Upgrade script for version 8.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Plugin_Upgrade_8_3_0 {
/**
* Check if the Gifting plugin is enabled and update the settings.
*
* @since 8.1.0
*/
public static function check_downloads_plugin_is_enabled() {
WCS_Upgrade_Logger::add( 'Checking if the Downloads plugin is enabled...' );
$active_plugins = get_option( 'active_plugins', array() );
$is_downloads_plugin_active = false;
foreach ( $active_plugins as $plugin ) {
if ( strpos( $plugin, 'woocommerce-subscription-downloads.php' ) !== false ) {
$is_downloads_plugin_active = true;
break;
}
}
if ( ! $is_downloads_plugin_active ) {
WCS_Upgrade_Logger::add( 'Downloads plugin is not enabled via active_plugins, skipping...' );
return;
}
WCS_Upgrade_Logger::add( 'Downloads plugin is enabled, updating Downloads settings...' );
update_option( WC_Subscriptions_Admin::$option_prefix . '_enable_downloadable_file_linking', 'yes' );
}
}

View File

@ -43,6 +43,8 @@ class WCS_Repair_Suspended_PayPal_Subscriptions extends WCS_Background_Upgrader
* @param int $subscription_id The ID of a shop_subscription/WC_Subscription object.
*/
protected function update_item( $subscription_id ) {
$subscription = null;
try {
$subscription = wcs_get_subscription( $subscription_id );

View File

@ -224,19 +224,19 @@ class WCS_Upgrade_1_2 {
// Upgrading with WC 1.x
if ( is_callable( array( $item_meta, 'add' ) ) ) {
$item_meta->add( '_subscription_period', $order_subscription_periods[ $product_id ] );
$item_meta->add( '_subscription_interval', $subscription_interval );
$item_meta->add( '_subscription_length', $subscription_length );
$item_meta->add( '_subscription_trial_length', $subscription_trial_length );
$item_meta->add( '_subscription_period', $order_subscription_periods[ $product_id ] ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$item_meta->add( '_subscription_interval', $subscription_interval ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$item_meta->add( '_subscription_length', $subscription_length ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$item_meta->add( '_subscription_trial_length', $subscription_trial_length ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$item_meta->add( '_subscription_recurring_amount', $order_item['line_subtotal'] ); // WC_Subscriptions_Product::get_price() would return a price without filters applied
$item_meta->add( '_subscription_sign_up_fee', $subscription_sign_up_fee );
$item_meta->add( '_subscription_recurring_amount', $order_item['line_subtotal'] ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$item_meta->add( '_subscription_sign_up_fee', $subscription_sign_up_fee ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
// Set recurring amounts for the item
$item_meta->add( '_recurring_line_total', $order_item['line_total'] );
$item_meta->add( '_recurring_line_tax', $order_item['line_tax'] );
$item_meta->add( '_recurring_line_subtotal', $order_item['line_subtotal'] );
$item_meta->add( '_recurring_line_subtotal_tax', $order_item['line_subtotal_tax'] );
$item_meta->add( '_recurring_line_total', $order_item['line_total'] ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$item_meta->add( '_recurring_line_tax', $order_item['line_tax'] ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$item_meta->add( '_recurring_line_subtotal', $order_item['line_subtotal'] ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$item_meta->add( '_recurring_line_subtotal_tax', $order_item['line_subtotal_tax'] ); // @phpstan-ignore method.notFound (Legacy upgrade code with proper defensive checks)
$order_item['item_meta'] = $item_meta->meta;

View File

@ -884,7 +884,6 @@ class WCS_Upgrade_2_0 {
* @param WC_Subscription $new_subscription A subscription object
* @param WC_Order $switch_order The original order used to purchase the subscription
* @param int $subscription_item_id The order item ID of the item added to the subscription by self::add_product()
* @return null
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
*/
private static function migrate_switch_meta( $new_subscription, $switch_order, $subscription_item_id ) {

View File

@ -79,11 +79,12 @@ function wcs_cart_totals_shipping_html() {
$chosen_recurring_method = empty( $package['rates'] ) ? '' : current( $package['rates'] )->id;
}
$is_package_found = isset( $package['rates'][ $chosen_initial_method ] ) && isset( $initial_packages[ $package_index ] );
$shipping_selection_displayed = false;
$only_one_shipping_option = count( $package['rates'] ) === 1;
$recurring_rates_match_initial_rates = isset( $package['rates'][ $chosen_initial_method ] ) && isset( $initial_packages[ $package_index ] ) && $package['rates'] == $initial_packages[ $package_index ]['rates']; // phpcs:ignore WordPress.PHP.StrictComparisons
$recurring_rates_match_initial_rates = WC_Subscriptions_Cart::package_rates_match_initial_rates( $initial_packages, $package, $recurring_cart_package_key, $recurring_cart );
if ( $only_one_shipping_option || ( $recurring_rates_match_initial_rates && apply_filters( 'wcs_cart_totals_shipping_html_price_only', true, $package, $recurring_cart ) ) ) {
if ( $only_one_shipping_option || ( $is_package_found && $recurring_rates_match_initial_rates ) ) {
$shipping_method = ( 1 === count( $package['rates'] ) ) ? current( $package['rates'] ) : $package['rates'][ $chosen_initial_method ];
// packages match, display shipping amounts only
?>

Some files were not shown because too many files have changed in this diff Show More