diff --git a/assets/css/admin.css b/assets/css/admin.css index 6980c64..90f9e19 100755 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -565,6 +565,40 @@ table.wp-list-table .subscription_renewal_order:after { } } +/* WooCommerce Payment Methods Settings page */ +.payment-method-features-info { + font-size: 1.4em; + display: inline-block; + text-indent: -9999px; + position: relative; + height: 1em; + width: 1em +} + +.payment-method-features-info::before { + font-family: WooCommerce; + speak: none; + font-weight: 400; + font-variant: normal; + text-transform: none; + line-height: 1; + margin: 0; + text-indent: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + content: "\e018"; + color: #a46497; +} + +table.wc_gateways .renewals .tips{ + margin: 0 0.2em; + display: inline-block; +} + /* Hide irrelevant sections on Edit Subscription screen */ body.post-type-shop_subscription .order_actions #actions optgroup[label='Resend order emails'], body.post-type-shop_subscription .add-items .description.tips, diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 11e0cd6..947c130 100755 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -11,6 +11,31 @@ color: #f29ec4; } +#woocommerce_dashboard_status .wc_status_list li.cancel-count { + width: 100%; +} + +#woocommerce_dashboard_status .wc_status_list li.cancel-count a:before { + content: "\e02c"; + color: #aa0000; +} + +#woocommerce_dashboard_status .wc_status_list li.signup-revenue a:before { + font-family: Dashicons; + content: '\f185'; + color: #5da5da; +} + +#woocommerce_dashboard_status .wc_status_list li.renewal-revenue a:before { + font-family: Dashicons; + content: '\f185'; + color: #f29ec4; +} + #woocommerce_dashboard_status .wc_status_list li.signup-count { border-right: 1px solid #ececec; } + +#woocommerce_dashboard_status .wc_status_list li.renewal-count { + border-right: 1px solid #ececec; +} diff --git a/assets/css/view-subscription.css b/assets/css/view-subscription.css index 1f40e8d..a8e8fe0 100755 --- a/assets/css/view-subscription.css +++ b/assets/css/view-subscription.css @@ -1,6 +1,58 @@ -.subscription_details .button { - margin-bottom: 2px; - width: 100%; - max-width: 200px; - text-align: center; +@media only screen and (max-width:768px) { + .subscription_details .button { + margin-bottom: 2px; + width: 100%; + max-width: 200px; + text-align: center; + } +} + +.subscription-auto-renew-toggle { + margin-left: 5px; + margin-bottom: 2px; + position: relative; + top: 4px; +} + +.subscription-auto-renew-toggle__i { + height: 20px; + width: 32px; + border: 2px solid #00BA8A; + background-color: #00BA8A; + display: inline-block; + text-indent: -9999px; + border-radius: 10em; + position: relative; + margin-top: -1px; + vertical-align: text-top; +} + +.subscription-auto-renew-toggle__i:before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: #fff; + position: absolute; + top: 0; + right: 0; + border-radius: 100%; +} + +.subscription-auto-renew-toggle--off .subscription-auto-renew-toggle__i { + border-color: #999; + background-color: #999; +} + +.subscription-auto-renew-toggle--off .subscription-auto-renew-toggle__i:before { + right: auto; + left: 0; +} + +.subscription-auto-renew-toggle--loading .subscription-auto-renew-toggle__i { + opacity: 0.5; +} + +.subscription-auto-renew-toggle--hidden { + display: none; } diff --git a/assets/js/admin/admin.js b/assets/js/admin/admin.js index d0eaff7..68bf856 100755 --- a/assets/js/admin/admin.js +++ b/assets/js/admin/admin.js @@ -766,4 +766,43 @@ jQuery(document).ready(function($){ }; wcs_prevent_product_type_change.init(); + /* + * Handles enabling and disabling PayPal Standard for Subscriptions. + */ + var wcs_paypal_standard_settings = { + + init: function() { + if ( 0 === $( '#woocommerce_paypal_enabled' ).length ) { + return; + } + + $( '#woocommerce_paypal_enabled' ).on( 'change', this.paypal_enabled_change ); + $( '#woocommerce_paypal_enabled_for_subscriptions' ).on( 'change', this.paypal_for_subscriptions_enabled ); + this.paypal_enabled_change(); + }, + + /** + * Show and hide the enable PayPal for Subscriptions checkbox when PayPal is enabled or disabled. + */ + paypal_enabled_change: function() { + var $enabled_for_subscriptions_element = $( '#woocommerce_paypal_enabled_for_subscriptions' ).closest( 'tr' ); + + if ( $( '#woocommerce_paypal_enabled' ).is( ':checked' ) ) { + $enabled_for_subscriptions_element.show(); + } else { + $enabled_for_subscriptions_element.hide(); + } + }, + + /** + * Display a confirm dialog when PayPal for Subscriptions is enabled (checked). + */ + paypal_for_subscriptions_enabled: function() { + if ( $( this ).is( ':checked' ) && ! confirm( WCSubscriptions.enablePayPalWarning ) ) { + $( this ).removeAttr( 'checked' ); + } + } + }; + wcs_paypal_standard_settings.init(); + }); diff --git a/assets/js/frontend/view-subscription.js b/assets/js/frontend/view-subscription.js new file mode 100755 index 0000000..c78db8f --- /dev/null +++ b/assets/js/frontend/view-subscription.js @@ -0,0 +1,107 @@ +jQuery( document ).ready( function( $ ) { + var $toggleContainer = $( '.wcs-auto-renew-toggle' ); + var $toggle = $( '.subscription-auto-renew-toggle', $toggleContainer ); + var $icon = $toggle.find( 'i' ); + var txtColor = null; + var $paymentMethod = $( '.subscription-payment-method' ); + + function getTxtColor() { + if ( !txtColor && ( $icon && $icon.length ) ) { + txtColor = getComputedStyle( $icon[0] ).color; + } + + return txtColor; + } + + function maybeApplyColor() { + if ( $toggle.hasClass( 'subscription-auto-renew-toggle--on' ) && $icon.length ) { + $icon[0].style.backgroundColor = getTxtColor(); + $icon[0].style.borderColor = getTxtColor(); + } else if( $icon.length ) { + $icon[0].style.backgroundColor = null; + $icon[0].style.borderColor = null; + } + } + + function displayToggle() { + $toggle.removeClass( 'subscription-auto-renew-toggle--hidden' ); + } + + function onToggle( e ) { + e.preventDefault(); + + // Remove focus from the toggle element. + $toggle.blur(); + + var ajaxHandler = function( action ) { + var data = { + subscription_id: WCSViewSubscription.subscription_id, + action: action, + security: WCSViewSubscription.auto_renew_nonce, + }; + + // While we're waiting for an AJAX response, block the toggle element to prevent spamming the server. + blockToggle(); + + $.ajax({ + url: WCSViewSubscription.ajax_url, + data: data, + type: 'POST', + success: function( result ) { + if ( result.payment_method ) { + $paymentMethod.fadeOut( function() { + $paymentMethod.html( result.payment_method ).fadeIn(); + }); + } + }, + error: function( jqxhr, status, exception ) { + alert( 'Exception:', exception ); + }, + complete: unblockToggle + }); + }; + + // Enable auto-renew + if ( $toggle.hasClass( 'subscription-auto-renew-toggle--off' ) ) { + // if payment method already exists just turn automatic renewals on. + if ( WCSViewSubscription.has_payment_gateway ) { + ajaxHandler( 'wcs_enable_auto_renew' ); + displayToggleOn(); + } else if ( window.confirm( WCSViewSubscription.add_payment_method_msg ) ) { // else add payment method + window.location.href = WCSViewSubscription.add_payment_method_url; + } + + } else { // Disable auto-renew + ajaxHandler( 'wcs_disable_auto_renew' ); + displayToggleOff(); + } + + maybeApplyColor(); + } + + function displayToggleOn() { + $icon.removeClass( 'fa-toggle-off' ).addClass( 'fa-toggle-on' ); + $toggle.removeClass( 'subscription-auto-renew-toggle--off' ).addClass( 'subscription-auto-renew-toggle--on' ); + } + + function displayToggleOff() { + $icon.removeClass( 'fa-toggle-on' ).addClass( 'fa-toggle-off' ); + $toggle.removeClass( 'subscription-auto-renew-toggle--on' ).addClass( 'subscription-auto-renew-toggle--off' ); + } + + function blockToggle() { + $toggleContainer.block({ + message: null, + overlayCSS: { opacity: 0.0 } + }); + } + + function unblockToggle() { + $toggleContainer.unblock(); + } + + $toggle.on( 'click', onToggle ); + maybeApplyColor(); + displayToggle(); +}); + diff --git a/assets/js/frontend/wcs-cart.js b/assets/js/frontend/wcs-cart.js index 89b8a11..834ca9f 100755 --- a/assets/js/frontend/wcs-cart.js +++ b/assets/js/frontend/wcs-cart.js @@ -10,8 +10,16 @@ function hide_non_applicable_coupons() { hide_non_applicable_coupons(); -jQuery( document ).ready( function( $ ){ +jQuery( document ).ready( function( $ ) { $( document.body ).on( 'updated_cart_totals updated_checkout', function() { hide_non_applicable_coupons(); } ); + + $( '.payment_methods [name="payment_method"]' ).click( function() { + if ( $( this ).hasClass( 'supports-payment-method-changes' ) ) { + $( '.update-all-subscriptions-payment-method-wrap' ).show(); + } else { + $( '.update-all-subscriptions-payment-method-wrap' ).hide(); + } + } ); } ); diff --git a/changelog.txt b/changelog.txt index 7bbda07..725f9e9 100755 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,77 @@ *** WooCommerce Subscriptions Changelog *** +2019.03.20 - version 2.5.3 +* New: Update Action Scheduler to version 2.2.1. PR#3266 +* Fix: Display the subscription status names (rather than the raw status) in the edit subscription related orders table. PR#3272 +* Fix: Use the correct endpoint for redirecting to the only available subscription on My Account. PR#3273 +* Fix: [WPML] Keep language request args in Subscription My Account endpoint URLs. PR#3068 +* Fix: Save the sale price schedule dates in UTC time (like WooCommerce core). PR#3270 +* Fix: Use site time when checking if payment date and trial end date are same. Fixes issues with synced products with a trial. PR#3264 +* Fix: Update retry report queries to refer to new retry tables. PR#3237 +* Fix: Do not display the PayPal Profile ID on the edit subscription screen when the subscription is Manual Renewal. PR#3259 +* Fix: Exclude `pending-cancel` subscriptions from anonymisation. PR#3256 +* Fix: Pass the user ID and gateway ID parameters in the right order. Fixes issues with bulk updating customer payment tokens. PR#3258 +* Fix: Temporarily lock payment on parent orders paid with PayPal Standard. Prevents customers paying twice while waiting for PayPal to respond. PR#2974 +* Fix: Remove HTML from strings passed through translation functions. PR#3067 +* Fix: Prevent endpoint settings having the same value. PR#3211 +* Fix: Remove `strtolower()` uses on translated text. Prevents lowercasing translated text which may have language specific capitalisation. PR#3251 +* Fix: Prevent possible fatal errors while populating the checkout with customer order data. Refactors the wcs_get_objects_property function. PR#3225 +* Fix: Removes use of absint for amounts/totals in reports. PR#3242 +* Fix: Display correct placeholder values for 'subject' and 'heading' e-mail settings #3246 +* Fix: Do not load the view-subscription scripts when subscription can't be viewed or doesn't exist. PR#3235 +* Fix: [WC3.6] Use new helper function to generate cart hash. PR#3215 +* Tweak: Use 'WC_Order_Item_Product::get_product' wherever possible in 'WC_Subscriptions_Synchroniser'. PR#3230 +* Tweak: Clarify the meaning of certain count values in subscription reports tool tips. PR#3143 +* Tweak: Fixes various typos. PR#3276 +* Dev: Add $cart as a parameter for the woocommerce_cart_subscription_string_details hook. PR#3257 + +2019.02.21 - version 2.5.2 +* Fix: Check if sale price is empty before returning it as the product price. Fixes compat. issues with Dynamic Pricing. PR#3213 +* Fix: Copy subscription meta data to early renewal orders. PR#3221 +* Fix: Add jquery-blockui as a dependency of the view-subscription JS file. PR#3227 +* Fix: Validate the DOM object exists before applying styles. Fixes JS errors on the My Account View Subscription page. PR#3218 +* Tweak: Escape line item names via wp_kses_post to allow safe html tags. PR#3224 +* Dev: Filter the wcs_cart_totals_shipping_method_price_label function to allow third-parties to alter the free shipping label. PR#3228 +* Dev: Add $variation parameter to 'woocommerce_subscriptions_product_needs_one_time_shipping' filter. PR#3197 + +2019.02.11 - version 2.5.1 +* Fix: In report queries use WordPress prefixed table names. PR#3205 +* Fix: Fix errors caused by calling WC_Gateway_Paypal::update_option() in early WooCommerce versions. PR#3208 +* Tweak: Rename the 'uncancel' admin subscription action to 'reactivate'. PR#3209 +* Enhancement: Add downloadable and virtual subscription options to the filter on the products admin page. PR#3199 + +2019.02.06 - version 2.5.0 +* New: Add payment gateway feature support tooltips to the WooCommerce>Setting>Payments screen. PR#3013 +* New: Add Subscriptions-related information to the WooCommerce Status Widget. PR#2980 +* New: Add option to allow a $0 initial sign-up without payment method. PR#3015 +* New: Update Action Scheduler to v2.2.0. PR#3187 +* New: Allow customers to update all subscriptions' payment method. PRs#3064,#3080,#3041 +* New: Display downloads in separate table on the customer's View Subscription page. PR#3128 +* New: Add new option to filter Admin subscriptions table by manual subscriptions. PR#3144 +* New: Add auto-renew toggle to Customer's View Subscription page. PR #3156 +* New: Add option to disable PayPal Standard for subscription-related purchases. PR#3152 +* New: Add change payment method link to the edit subscription screen. PR#3086 +* New: Allow store managers and customers to uncancel a subscription while it's still in a pending-cancel status. PR#2993 +* Fix: Prevent PHP warning in WC_Subscriptions_Cart::cart_needs_payment(). PR#3142 +* Fix: Do not return non-order objects from WC_Subscription::get_related_orders( 'all' ). Prevents errors when the calling function expects an order but got false. PR#3140 +* Fix: Don't display the subscriptions My Account menu item when endpoint is empty. PR#3146 +* Fix: Fix rounding warnings with PayPal Reference Transactions. PR#3134 +* Fix: Remove use of an invalid Exception object type. PR#3148 +* Fix: Fallback to previous renewal when parent order is not define for PayPal Standard IPNs. PR#2855 +* Fix: Update WC_Subscription::status_transition() method to be inline with WC_Order::status_transition(). PR#2777 +* Fix: Skip non-orders objects when generating the related orders table to prevent fatal errors. PR#3161 +* Fix: Redirect non-logged in users to login when handling requests to change a payment method. PR#3160 +* Fix: Don't apply retry rules to manual renewal payment attempts. PR#3153 +* Tweak: Revert subscription customer ID and post_author changes introduced for WC3.5. PR#3159 +* Tweak: Update failed scheduled action notice content. PR#3170 +* Tweak: Ignore all $0.01 PayPal Standard IPN transactions. PR#3178 +* Tweak: Add installed plugin information to the log file during plugin upgrade. PR#3181 +* Dev: Add WC_Subscription::get_change_payment_method_url() function. PR#3041 +* Dev: Add a filter to allow custom relations to be supported by the related order store. PR#3089 +* Dev: Update uses of woocommerce_order_item_meta_end to use add_action, remove_action - not filter APIs. #3163 +* Dev: [WC 3.5.4] Subscription order key updates. PR#3171 +* Dev: Deprecate WC_Subscriptions_Cart::pre_get_refreshed_fragments. PR#3173 + 2018.12.21 - version 2.4.7 * Fix: Fix an issue with the release date of 2.4.6. diff --git a/composer.lock b/composer.lock deleted file mode 100755 index 51054e4..0000000 --- a/composer.lock +++ /dev/null @@ -1,1252 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "content-hash": "021c140a843bd5b969f18f6e06902332", - "packages": [ - { - "name": "composer/installers", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/composer/installers.git", - "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b", - "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0" - }, - "replace": { - "roundcube/plugin-installer": "*", - "shama/baton": "*" - }, - "require-dev": { - "composer/composer": "1.0.*@dev", - "phpunit/phpunit": "4.1.*" - }, - "type": "composer-plugin", - "extra": { - "class": "Composer\\Installers\\Plugin", - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Installers\\": "src/Composer/Installers" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kyle Robinson Young", - "email": "kyle@dontkry.com", - "homepage": "https://github.com/shama" - } - ], - "description": "A multi-framework Composer library installer", - "homepage": "https://composer.github.io/installers/", - "keywords": [ - "Craft", - "Dolibarr", - "Eliasis", - "Hurad", - "ImageCMS", - "Kanboard", - "Lan Management System", - "MODX Evo", - "Mautic", - "Maya", - "OXID", - "Plentymarkets", - "Porto", - "RadPHP", - "SMF", - "Thelia", - "WolfCMS", - "agl", - "aimeos", - "annotatecms", - "attogram", - "bitrix", - "cakephp", - "chef", - "cockpit", - "codeigniter", - "concrete5", - "croogo", - "dokuwiki", - "drupal", - "eZ Platform", - "elgg", - "expressionengine", - "fuelphp", - "grav", - "installer", - "itop", - "joomla", - "kohana", - "laravel", - "lavalite", - "lithium", - "magento", - "mako", - "mediawiki", - "modulework", - "moodle", - "osclass", - "phpbb", - "piwik", - "ppi", - "puppet", - "reindex", - "roundcube", - "shopware", - "silverstripe", - "sydes", - "symfony", - "typo3", - "wordpress", - "yawik", - "zend", - "zikula" - ], - "time": "2017-08-09T07:53:48+00:00" - } - ], - "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", - "shasum": "" - }, - "require": { - "php": ">=5.3,<8.0-DEV" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2015-06-14T21:17:01+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2015-12-27T11:43:31+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "3.2.2", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.3.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-08T06:39:58+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2017-06-03T08:32:36+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.7.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", - "sebastian/comparator": "^1.1|^2.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2017-03-02T20:05:34+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "2.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0" - }, - "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.2.1", - "ext-xmlwriter": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2015-10-06T15:47:00+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2016-10-03T07:40:28+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2017-02-26T11:10:40+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "1.4.11", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2017-02-27T10:12:30+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "4.8.36", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "~2.1", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "~2.3", - "sebastian/comparator": "~1.2.2", - "sebastian/diff": "~1.2", - "sebastian/environment": "~1.3", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/version": "~1.0", - "symfony/yaml": "~2.1|~3.0" - }, - "suggest": { - "phpunit/php-invoker": "~1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.8.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2017-06-21T08:07:12+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "2.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": ">=5.3.3", - "phpunit/php-text-template": "~1.2", - "sebastian/exporter": "~1.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2015-10-02T06:51:40+00:00" - }, - { - "name": "sebastian/comparator", - "version": "1.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2017-01-29T09:50:25+00:00" - }, - { - "name": "sebastian/diff", - "version": "1.4.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-05-22T07:24:03+00:00" - }, - { - "name": "sebastian/environment", - "version": "1.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2016-08-18T05:49:44+00:00" - }, - { - "name": "sebastian/exporter", - "version": "1.2.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2016-06-17T09:04:28+00:00" - }, - { - "name": "sebastian/global-state", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2015-10-12T03:26:01+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-10-03T07:41:43+00:00" - }, - { - "name": "sebastian/version", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21T13:59:46+00:00" - }, - { - "name": "symfony/yaml", - "version": "v3.3.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed", - "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "require-dev": { - "symfony/console": "~2.8|~3.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2017-07-23T12:43:26+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2016-11-23T20:04:58+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} diff --git a/includes/abstracts/abstract-wcs-related-order-store.php b/includes/abstracts/abstract-wcs-related-order-store.php index bf89b9e..b7754cd 100755 --- a/includes/abstracts/abstract-wcs-related-order-store.php +++ b/includes/abstracts/abstract-wcs-related-order-store.php @@ -41,11 +41,7 @@ abstract class WCS_Related_Order_Store { * * @var array */ - private static $relation_type_keys = array( - 'renewal' => true, - 'switch' => true, - 'resubscribe' => true, - ); + private static $relation_type_keys = array(); /** * Get the active related order data store. @@ -59,6 +55,19 @@ abstract class WCS_Related_Order_Store { wcs_doing_it_wrong( __METHOD__, 'This method was called before the "plugins_loaded" hook. It applies a filter to the related order data store instantiated. For that to work, it should first be called after all plugins are loaded.', '2.3.0' ); } + /** + * Allow third-parties to register their own custom order relationship types which should be handled by this store. + * + * @param array An array of order relationship types. + * @since 2.5.0 + */ + foreach ( (array) apply_filters( 'wcs_additional_related_order_relation_types', array() ) as $relation_type ) { + self::$relation_types[] = $relation_type; + + } + + self::$relation_type_keys = array_fill_keys( self::$relation_types, true ); + $class = apply_filters( 'wcs_related_order_store_class', 'WCS_Related_Order_Store_Cached_CPT' ); self::$instance = new $class(); self::$instance->init(); diff --git a/includes/admin/class-wc-subscriptions-admin.php b/includes/admin/class-wc-subscriptions-admin.php index 6cd4428..5449821 100755 --- a/includes/admin/class-wc-subscriptions-admin.php +++ b/includes/admin/class-wc-subscriptions-admin.php @@ -59,6 +59,10 @@ class WC_Subscriptions_Admin { // Add subscriptions to the product select box add_filter( 'product_type_selector', __CLASS__ . '::add_subscription_products_to_select' ); + // Special handling of downloadable and virtual products on the WooCommerce > Products screen. + add_filter( 'product_type_selector', array( __CLASS__, 'add_downloadable_and_virtual_filters' ) ); + add_filter( 'request', array( __CLASS__, 'modify_downloadable_and_virtual_product_queries' ), 11 ); + // Add subscription pricing fields on edit product page add_action( 'woocommerce_product_options_general_product_data', __CLASS__ . '::subscription_pricing_fields' ); @@ -106,15 +110,17 @@ class WC_Subscriptions_Admin { add_filter( 'posts_where', __CLASS__ . '::filter_orders' ); + add_filter( 'posts_where', array( __CLASS__, 'filter_paid_subscription_orders_for_user' ) ); + add_action( 'admin_notices', __CLASS__ . '::display_renewal_filter_notice' ); add_shortcode( 'subscriptions', __CLASS__ . '::do_subscriptions_shortcode' ); add_filter( 'set-screen-option', __CLASS__ . '::set_manage_subscriptions_screen_option', 10, 3 ); - add_filter( 'woocommerce_payment_gateways_setting_columns', __CLASS__ . '::payment_gateways_rewewal_column' ); + add_filter( 'woocommerce_payment_gateways_setting_columns', array( __CLASS__, 'payment_gateways_renewal_column' ) ); - add_action( 'woocommerce_payment_gateways_setting_column_renewals', __CLASS__ . '::payment_gateways_rewewal_support' ); + add_action( 'woocommerce_payment_gateways_setting_column_renewals', array( __CLASS__, 'payment_gateways_renewal_support' ) ); // Do not display formatted order total on the Edit Order administration screen add_filter( 'woocommerce_get_formatted_order_total', __CLASS__ . '::maybe_remove_formatted_order_total_filter', 0, 2 ); @@ -188,6 +194,74 @@ class WC_Subscriptions_Admin { return $product_types; } + /** + * Add options for downloadable and virtual subscription products to the product type selector on the WooCommerce products screen. + * + * @param array $product_types + * @return array + * @since 2.5.1 + */ + public static function add_downloadable_and_virtual_filters( $product_types ) { + global $typenow; + + if ( ! is_admin() || ! doing_action( 'restrict_manage_posts' ) || 'product' !== $typenow ) { + return $product_types; + } + + $product_options = array_reverse( + array( + 'downloadable_subscription' => ( is_rtl() ? '←' : '→' ) . ' ' . __( 'Downloadable', 'woocommerce-subscriptions' ), + 'virtual_subscription' => ( is_rtl() ? '←' : '→' ) . ' ' . __( 'Virtual', 'woocommerce-subscriptions' ), + ) + ); + foreach ( $product_options as $key => $label ) { + $product_types = wcs_array_insert_after( 'subscription', $product_types, $key, $label ); + } + + return $product_types; + } + + /** + * Modifies the main query on the WooCommerce products screen to correctly handle filtering by virtual and downloadable + * product types. + * + * @param array $query_vars + * @return array $query_vars + * @since 2.5.1 + */ + public static function modify_downloadable_and_virtual_product_queries( $query_vars) { + global $pagenow, $typenow; + + if ( ! is_admin() || 'edit.php' !== $pagenow || 'product' !== $typenow ) { + return $query_vars; + } + + $current_product_type = isset( $_REQUEST['product_type'] ) ? wc_clean( wp_unslash( $_REQUEST['product_type'] ) ) : false; + + if ( ! $current_product_type ) { + return $query_vars; + } + + if ( in_array( $current_product_type, array( 'downloadable', 'virtual' ) ) && ! isset( $query_vars['tax_query'] ) ) { + // Do not include subscriptions when the default "Downloadable" or "Virtual" query for simple products is being executed. + $query_vars['tax_query'] = array( + array( + 'taxonomy' => 'product_type', + 'terms' => array( 'subscription' ), + 'field' => 'slug', + 'operator' => 'NOT IN', + ), + ); + } elseif ( in_array( $current_product_type, array( 'downloadable_subscription', 'virtual_subscription' ) ) ) { + // Limit query to subscription products when the "Downloadable" or "Virtual" choices under "Simple Subscription" are being used. + $query_vars['meta_value'] = 'yes'; + $query_vars['meta_key'] = '_' . str_replace( '_subscription', '', $current_product_type ); + $query_vars['product_type'] = 'subscription'; + } + + return $query_vars; + } + /** * Output the subscription specific pricing fields on the "Edit Product" admin page. * @@ -247,7 +321,8 @@ class WC_Subscriptions_Admin { // Sign-up Fee woocommerce_wp_text_input( array( 'id' => '_subscription_sign_up_fee', - 'class' => 'wc_input_subscription_intial_price wc_input_price short', + // Keep wc_input_subscription_intial_price for backward compatibility. + 'class' => 'wc_input_subscription_intial_price wc_input_subscription_initial_price wc_input_price short', // translators: %s is a currency symbol / code 'label' => sprintf( __( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ), 'placeholder' => _x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ), @@ -389,8 +464,11 @@ class WC_Subscriptions_Admin { update_post_meta( $post_id, '_regular_price', $subscription_price ); update_post_meta( $post_id, '_sale_price', $sale_price ); - $date_from = ( isset( $_POST['_sale_price_dates_from'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_from'] ) : ''; - $date_to = ( isset( $_POST['_sale_price_dates_to'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_to'] ) : ''; + $site_offset = get_option( 'gmt_offset' ) * 3600; + + // Save the timestamps in UTC time, the way WC does it. + $date_from = ( isset( $_POST['_sale_price_dates_from'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_from'] ) - $site_offset : ''; + $date_to = ( isset( $_POST['_sale_price_dates_to'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_to'] ) - $site_offset : ''; $now = gmdate( 'U' ); @@ -746,12 +824,12 @@ class WC_Subscriptions_Admin { 'productHasSubscriptions' => wcs_get_subscriptions_for_product( $post->ID ) ? 'yes' : 'no', 'productTypeWarning' => __( 'Product type can not be changed because this product is associated with active subscriptions', 'woocommerce-subscriptions' ), ); - } else if ( 'edit-shop_order' == $screen->id ) { + } elseif ( 'edit-shop_order' == $screen->id ) { $script_params = array( 'bulkTrashWarning' => __( "You are about to trash one or more orders which contain a subscription.\n\nTrashing the orders will also trash the subscriptions purchased with these orders.", 'woocommerce-subscriptions' ), 'trashWarning' => $trashing_subscription_order_warning, ); - } else if ( 'shop_order' == $screen->id ) { + } elseif ( 'shop_order' == $screen->id ) { $dependencies[] = $woocommerce_admin_script_handle; $dependencies[] = 'wc-admin-order-meta-boxes'; @@ -767,10 +845,14 @@ class WC_Subscriptions_Admin { 'EditOrderNonce' => wp_create_nonce( 'woocommerce-subscriptions' ), 'postId' => $post->ID, ); - } else if ( 'users' == $screen->id ) { + } elseif ( 'users' == $screen->id ) { $script_params = array( 'deleteUserWarning' => __( "Warning: Deleting a user will also delete the user's subscriptions. The user's orders will remain but be reassigned to the 'Guest' user.\n\nDo you want to continue to delete this user and any associated subscriptions?", 'woocommerce-subscriptions' ), ); + } elseif ( 'woocommerce_page_wc-settings' === $screen->id ) { + $script_params = array( + 'enablePayPalWarning' => __( 'PayPal Standard has a number of limitations and does not support all subscription features.', 'woocommerce-subscriptions' ) . "\n\n" . __( 'Because of this, it is not recommended as a payment method for Subscriptions unless it is the only available option for your country.', 'woocommerce-subscriptions' ), + ); } $script_params['ajaxLoaderImage'] = WC()->plugin_url() . '/assets/images/ajax-loader.gif'; @@ -1179,6 +1261,15 @@ class WC_Subscriptions_Admin { 'desc_tip' => __( 'Allow a subscription product to be purchased with other products and subscriptions in the same transaction.', 'woocommerce-subscriptions' ), ), + array( + 'name' => __( '$0 Initial Checkout', 'woocommerce-subscriptions' ), + 'desc' => __( 'Allow $0 initial checkout without a payment method.', 'woocommerce-subscriptions' ), + 'id' => self::$option_prefix . '_zero_initial_payment_requires_payment', + 'default' => 'no', + 'type' => 'checkbox', + 'desc_tip' => __( 'Allow a subscription product with a $0 initial payment to be purchased without providing a payment method. The customer will be required to provide a payment method at the end of the initial period to keep the subscription active.', 'woocommerce-subscriptions' ), + ), + array( 'name' => __( 'Drip Downloadable Content', 'woocommerce-subscriptions' ), 'desc' => __( 'Enable dripping for downloadable content on subscription products.', 'woocommerce-subscriptions' ), @@ -1301,9 +1392,7 @@ class WC_Subscriptions_Admin { public static function filter_orders( $where ) { global $typenow, $wpdb; - if ( is_admin() && 'shop_order' == $typenow ) { - - $related_orders = array(); + if ( is_admin() && 'shop_order' === $typenow ) { if ( isset( $_GET['_subscription_related_orders'] ) && $_GET['_subscription_related_orders'] > 0 ) { @@ -1325,6 +1414,45 @@ class WC_Subscriptions_Admin { return $where; } + /** + * Filter the "Orders" list to show only paid subscription orders for a particular user + * + * @param string $where + * @return string + * @since 2.5.3 + */ + public static function filter_paid_subscription_orders_for_user( $where ) { + global $typenow, $wpdb; + + if ( ! is_admin() || 'shop_order' !== $typenow || ! isset( $_GET['_paid_subscription_orders_for_customer_user'] ) || 0 == $_GET['_paid_subscription_orders_for_customer_user'] ) { + return $where; + } + + $user_id = $_GET['_paid_subscription_orders_for_customer_user']; + + // Unset the GET arg so that it doesn't interfere with the query for user's subscriptions. + unset( $_GET['_paid_subscription_orders_for_customer_user'] ); + + $users_subscriptions = wcs_get_users_subscriptions( $user_id ); + + $users_subscription_orders = array(); + + foreach ( $users_subscriptions as $subscription ) { + $users_subscription_orders = array_merge( $users_subscription_orders, $subscription->get_related_orders( 'ids' ) ); + } + + if ( empty( $users_subscription_orders ) ) { + wcs_add_admin_notice( sprintf( __( 'We can\'t find a paid subscription order for this user.', 'woocommerce-subscriptions' ) ), 'error' ); + $where .= " AND {$wpdb->posts}.ID = 0"; + } else { + // Orders with paid status + $where .= sprintf( " AND {$wpdb->posts}.post_status IN ( 'wc-processing', 'wc-completed' )" ); + $where .= sprintf( " AND {$wpdb->posts}.ID IN (%s)", implode( ',', array_unique( $users_subscription_orders ) ) ); + } + + return $where; + } + /** * Display a notice indicating that the "Orders" list is filtered. * @see self::filter_orders() @@ -1473,16 +1601,29 @@ class WC_Subscriptions_Admin { /** * Add a column to the Payment Gateway table to show whether the gateway supports automated renewals. * - * @since 1.5 + * @param array $header + * + * @since 2.5.3 + * @return array + */ + public static function payment_gateways_renewal_column( $header ) { + $header_new = array_slice( $header, 0, count( $header ) - 1, true ) + array( 'renewals' => __( 'Automatic Recurring Payments', 'woocommerce-subscriptions' ) ) + // Ideally, we could add a link to the docs here, but the title is passed through esc_html() + array_slice( $header, count( $header ) - 1, count( $header ) - ( count( $header ) - 1 ), true ); + + return $header_new; + } + + /** + * Add a column to the Payment Gateway table to show whether the gateway supports automated renewals. + * + * @since 1.5 + * @deprecated 2.5.3 * @return string */ public static function payment_gateways_rewewal_column( $header ) { + wcs_deprecated_function( __METHOD__, '2.5.3', 'WC_Subscriptions_Admin::payment_gateways_renewal_column( $header )' ); - $header_new = array_slice( $header, 0, count( $header ) - 1, true ) + - array( 'renewals' => __( 'Automatic Recurring Payments', 'woocommerce-subscriptions' ) ) + // Ideally, we could add a link to the docs here, but the title is passed through esc_html() - array_slice( $header, count( $header ) - 1, count( $header ) - ( count( $header ) - 1 ), true ); - - return $header_new; + return self::payment_gateways_renewal_column( $header ); } /** @@ -1490,10 +1631,11 @@ class WC_Subscriptions_Admin { * Automatically flag support for Paypal since it is included with subscriptions. * Display in the Payment Gateway column. * - * @since 1.5 + * @param WC_Payment_Gateway $gateway + * + * @since 2.5.3 */ - public static function payment_gateways_rewewal_support( $gateway ) { - + public static function payment_gateways_renewal_support( $gateway ) { echo ''; if ( ( is_array( $gateway->supports ) && in_array( 'subscriptions', $gateway->supports ) ) || $gateway->id == 'paypal' ) { $status_html = '' . esc_html__( 'Yes', 'woocommerce-subscriptions' ) . ''; @@ -1501,14 +1643,15 @@ class WC_Subscriptions_Admin { $status_html = '-'; } - $allowed_html = wp_kses_allowed_html( 'post' ); + $allowed_html = wp_kses_allowed_html( 'post' ); $allowed_html['span']['data-tip'] = true; /** * Automatic Renewal Payments Support Status HTML Filter. * * @since 2.0 - * @param string $status_html + * + * @param string $status_html * @param \WC_Payment_Gateway $gateway */ echo wp_kses( apply_filters( 'woocommerce_payment_gateways_renewal_support_status_html', $status_html, $gateway ), $allowed_html ); @@ -1516,6 +1659,20 @@ class WC_Subscriptions_Admin { echo ''; } + /** + * Check whether the payment gateway passed in supports automated renewals or not. + * Automatically flag support for Paypal since it is included with subscriptions. + * Display in the Payment Gateway column. + * + * @since 1.5 + * @deprecated 2.5.3 + */ + public static function payment_gateways_rewewal_support( $gateway ) { + wcs_deprecated_function( __METHOD__, '2.5.3', 'WC_Subscriptions_Admin::payment_gateways_renewal_support( $gateway )' ); + + return self::payment_gateways_renewal_support( $gateway ); + } + /** * Do not display formatted order total on the Edit Order administration screen * diff --git a/includes/admin/class-wcs-admin-post-types.php b/includes/admin/class-wcs-admin-post-types.php index 50aa546..43f3c97 100755 --- a/includes/admin/class-wcs-admin-post-types.php +++ b/includes/admin/class-wcs-admin-post-types.php @@ -495,7 +495,7 @@ class WCS_Admin_Post_Types { } } else { - if ( 'pending-cancel' === $the_subscription->get_status() ) { + if ( 'cancelled' === $status && 'pending-cancel' === $the_subscription->get_status() ) { $label = __( 'Cancel Now', 'woocommerce-subscriptions' ); } @@ -772,20 +772,29 @@ class WCS_Admin_Post_Types { } if ( ! empty( $_GET['_payment_method'] ) ) { - - $payment_gateway_filter = ( 'none' == $_GET['_payment_method'] ) ? '' : $_GET['_payment_method']; - - $query_vars = array( - 'post_type' => 'shop_subscription', - 'posts_per_page' => -1, - 'post_status' => 'any', - 'fields' => 'ids', - 'meta_query' => array( + if ( '_manual_renewal' === trim( $_GET['_payment_method'] ) ) { + $meta_query = array( + array( + 'key' => '_requires_manual_renewal', + 'value' => 'true', + ), + ); + } else { + $payment_gateway_filter = ( 'none' == $_GET['_payment_method'] ) ? '' : $_GET['_payment_method']; + $meta_query = array( array( 'key' => '_payment_method', 'value' => $payment_gateway_filter, ), - ), + ); + } + + $query_vars = array( + 'post_type' => 'shop_subscription', + 'posts_per_page' => -1, + 'post_status' => 'any', + 'fields' => 'ids', + 'meta_query' => $meta_query, ); // If there are already set post restrictions (post__in) apply them to this query @@ -934,7 +943,9 @@ class WCS_Admin_Post_Types { foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) { echo ''; - }?> + } + echo ''; + ?> 1 ) { $item_name = sprintf( '%s × %s', absint( $item_quantity ), $item_name ); diff --git a/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php b/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php index 79e5f96..7eb9067 100755 --- a/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php +++ b/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php @@ -164,7 +164,7 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data { echo 'get_payment_method() ) ? ' class="' . esc_attr( $subscription->get_payment_method() ) . '"' : '' ) . '>' . esc_html__( 'Payment Method', 'woocommerce-subscriptions' ) . ':' . wp_kses_post( nl2br( $subscription->get_payment_method_to_display() ) ); // Display help tip - if ( '' != $subscription->get_payment_method() && ! $subscription->is_manual() ) { + if ( '' != $subscription->get_payment_method() && ! $subscription->is_manual() ) { echo wcs_help_tip( sprintf( _x( 'Gateway ID: [%s]', 'The gateway ID displayed on the Edit Subscriptions screen when editing payment method.', 'woocommerce-subscriptions' ), $subscription->get_payment_method() ) ); } @@ -197,6 +197,22 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data { echo ''; do_action( 'woocommerce_admin_order_data_after_billing_address', $subscription ); + + // Display a link to the customer's add/change payment method screen. + if ( $subscription->can_be_updated_to( 'new-payment-method' ) ) { + + if ( $subscription->has_payment_gateway() ) { + $link_text = __( 'Customer change payment method page →', 'woocommerce-subscriptions' ); + } else { + $link_text = __( 'Customer add payment method page →', 'woocommerce-subscriptions' ); + } + + printf( + '%s', + esc_url( $subscription->get_change_payment_method_url() ), + esc_html( $link_text ) + ); + } ?>
@@ -302,8 +318,7 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data { // Ensure there is an order key. if ( ! $subscription->get_order_key() ) { - $key = 'wc_' . apply_filters( 'woocommerce_generate_order_key', uniqid( 'order_' ) ); - wcs_set_objects_property( $subscription, 'order_key', $key ); + wcs_set_objects_property( $subscription, 'order_key', wcs_generate_order_key() ); } // Update meta diff --git a/includes/admin/meta-boxes/views/html-related-orders-row.php b/includes/admin/meta-boxes/views/html-related-orders-row.php index f040401..c733b0e 100755 --- a/includes/admin/meta-boxes/views/html-related-orders-row.php +++ b/includes/admin/meta-boxes/views/html-related-orders-row.php @@ -28,7 +28,7 @@ $order_post = wcs_get_objects_property( $order, 'post' ); if ( $timestamp_gmt > 0 ) { // translators: php date format $t_time = get_the_time( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $order_post ); - $date_to_display = wcs_get_human_time_diff( $timestamp_gmt ); + $date_to_display = ucfirst( wcs_get_human_time_diff( $timestamp_gmt ) ); } else { $t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' ); } ?> @@ -37,7 +37,13 @@ $order_post = wcs_get_objects_property( $order, 'post' ); - get_status() ) ); ?> + get_status( 'view' ) ) ); + } else { + echo esc_html( wc_get_order_status_name( $order->get_status( 'view' ) ) ); + } + ?> get_formatted_order_total(), array( 'small' => array(), 'span' => array( 'class' => array() ), 'del' => array(), 'ins' => array() ) ); ?> diff --git a/includes/admin/meta-boxes/views/html-retries-table.php b/includes/admin/meta-boxes/views/html-retries-table.php index d5b6bc7..b5d3935 100755 --- a/includes/admin/meta-boxes/views/html-retries-table.php +++ b/includes/admin/meta-boxes/views/html-retries-table.php @@ -42,7 +42,7 @@ if ( ! defined( 'ABSPATH' ) ) { if ( $retry->get_time() > 0 ) { // translators: php date format $t_time = date( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $retry->get_time() ); - $date_to_display = wcs_get_human_time_diff( $retry->get_time() ); + $date_to_display = ucfirst( wcs_get_human_time_diff( $retry->get_time() ) ); } else { $t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' ); } ?> diff --git a/includes/admin/reports/class-wcs-report-cache-manager.php b/includes/admin/reports/class-wcs-report-cache-manager.php index eb40b48..dcc3d05 100755 --- a/includes/admin/reports/class-wcs-report-cache-manager.php +++ b/includes/admin/reports/class-wcs-report-cache-manager.php @@ -27,35 +27,38 @@ class WCS_Report_Cache_Manager { */ private $update_events_and_classes = array( 'woocommerce_subscriptions_reports_schedule_cache_updates' => array( // a custom hook that can be called to schedule a full cache update, used by WC_Subscriptions_Upgrader - 0 => 'WCS_Report_Subscription_Events_By_Date', - 1 => 'WCS_Report_Upcoming_Recurring_Revenue', - 3 => 'WCS_Report_Subscription_By_Product', - 4 => 'WCS_Report_Subscription_By_Customer', + 0 => 'WCS_Report_Dashboard', + 1 => 'WCS_Report_Subscription_Events_By_Date', + 2 => 'WCS_Report_Upcoming_Recurring_Revenue', + 4 => 'WCS_Report_Subscription_By_Product', + 5 => 'WCS_Report_Subscription_By_Customer', ), 'woocommerce_subscription_payment_complete' => array( // this hook takes care of renewal, switch and initial payments - 0 => 'WCS_Report_Subscription_Events_By_Date', - 4 => 'WCS_Report_Subscription_By_Customer', + 0 => 'WCS_Report_Dashboard', + 1 => 'WCS_Report_Subscription_Events_By_Date', + 5 => 'WCS_Report_Subscription_By_Customer', ), 'woocommerce_subscriptions_switch_completed' => array( - 0 => 'WCS_Report_Subscription_Events_By_Date', + 1 => 'WCS_Report_Subscription_Events_By_Date', ), 'woocommerce_subscription_status_changed' => array( - 0 => 'WCS_Report_Subscription_Events_By_Date', // we really only need cancelled, expired and active status here, but we'll use a more generic hook for convenience - 4 => 'WCS_Report_Subscription_By_Customer', + 0 => 'WCS_Report_Dashboard', + 1 => 'WCS_Report_Subscription_Events_By_Date', // we really only need cancelled, expired and active status here, but we'll use a more generic hook for convenience + 5 => 'WCS_Report_Subscription_By_Customer', ), 'woocommerce_subscription_status_active' => array( - 1 => 'WCS_Report_Upcoming_Recurring_Revenue', + 2 => 'WCS_Report_Upcoming_Recurring_Revenue', ), 'woocommerce_new_order_item' => array( - 3 => 'WCS_Report_Subscription_By_Product', + 4 => 'WCS_Report_Subscription_By_Product', ), 'woocommerce_update_order_item' => array( - 3 => 'WCS_Report_Subscription_By_Product', + 4 => 'WCS_Report_Subscription_By_Product', ), ); /** - * Record of all the report calsses to need to have the cache updated during this request. Prevents duplicate updates in the same request for different events. + * Record of all the report classes to need to have the cache updated during this request. Prevents duplicate updates in the same request for different events. */ private $reports_to_update = array(); @@ -148,9 +151,9 @@ class WCS_Report_Cache_Manager { $cron_args = array( 'report_class' => $report_class ); - if ( false === wp_next_scheduled( $this->cron_hook, $cron_args ) ) { + if ( false === as_next_scheduled_action( $this->cron_hook, $cron_args ) ) { // Use the index to space out caching of each report to make them 15 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request - wp_schedule_single_event( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args ); + as_schedule_single_action( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args ); } } } else { // Otherwise, run it 10 minutes after the last cache invalidating event @@ -160,12 +163,12 @@ class WCS_Report_Cache_Manager { $cron_args = array( 'report_class' => $report_class ); - if ( false !== ( $next_scheduled = wp_next_scheduled( $this->cron_hook, $cron_args ) ) ) { - wp_unschedule_event( $next_scheduled, $this->cron_hook, $cron_args ); + if ( false !== as_next_scheduled_action( $this->cron_hook, $cron_args ) ) { + as_unschedule_action( $this->cron_hook, $cron_args ); } // Use the index to space out caching of each report to make them 5 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request - wp_schedule_single_event( gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args ); + as_schedule_single_action( gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args ); } } } diff --git a/includes/admin/reports/class-wcs-report-dashboard.php b/includes/admin/reports/class-wcs-report-dashboard.php index 748d869..6e14f3e 100755 --- a/includes/admin/reports/class-wcs-report-dashboard.php +++ b/includes/admin/reports/class-wcs-report-dashboard.php @@ -30,13 +30,29 @@ class WCS_Report_Dashboard { } /** - * Add the subscription specific details to the bottom of the dashboard widget - * - * @since 2.1 + * Get all data needed for this report and store in the class */ - public static function add_stats_to_dashboard() { + public static function get_data( $args = array() ) { global $wpdb; + $default_args = array( + 'no_cache' => false, + ); + + $args = apply_filters( 'wcs_reports_subscription_dashboard_args', $args ); + $args = wp_parse_args( $args, $default_args ); + + $offset = get_option( 'gmt_offset' ); + + // Use this once it is merged - wcs_get_gmt_offset_string(); + // Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query. + $site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 ); + + $report_data = new stdClass; + + $cached_results = get_transient( strtolower( self::class ) ); + + // Subscription signups this month $query = $wpdb->prepare( "SELECT COUNT(DISTINCT wcsubs.ID) AS count FROM {$wpdb->posts} AS wcsubs @@ -48,11 +64,51 @@ class WCS_Report_Dashboard { AND wcorder.post_date >= '%s' AND wcorder.post_date < '%s'", date( 'Y-m-01', current_time( 'timestamp' ) ), - date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) + date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ) ); - $signup_count = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) ); + $query_hash = md5( $query ); + if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { + $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); + $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) ); + set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS ); + } + + $report_data->signup_count = $cached_results[ $query_hash ]; + + // Signup revenue this month + $query = $wpdb->prepare( + "SELECT SUM(order_total_meta.meta_value) + FROM {$wpdb->postmeta} AS order_total_meta + RIGHT JOIN + ( + SELECT DISTINCT wcorder.ID + FROM {$wpdb->posts} AS wcsubs + INNER JOIN {$wpdb->posts} AS wcorder + ON wcsubs.post_parent = wcorder.ID + WHERE wcorder.post_type IN ( 'shop_order' ) + AND wcsubs.post_type IN ( 'shop_subscription' ) + AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' ) + AND wcorder.post_date >= '%s' + AND wcorder.post_date < '%s' + ) AS orders ON orders.ID = order_total_meta.post_id + WHERE order_total_meta.meta_key = '_order_total'", + date( 'Y-m-01', current_time( 'timestamp' ) ), + date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ) + ); + + $query_hash = md5( $query ); + + if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { + $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); + $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query ) ); + set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS ); + } + + $report_data->signup_revenue = $cached_results[ $query_hash ]; + + // Subscription renewals this month $query = $wpdb->prepare( "SELECT COUNT(DISTINCT wcorder.ID) AS count FROM {$wpdb->posts} AS wcorder @@ -67,20 +123,119 @@ class WCS_Report_Dashboard { AND wcorder.post_date >= '%s' AND wcorder.post_date < '%s'", date( 'Y-m-01', current_time( 'timestamp' ) ), - date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) + date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ) ); - $renewal_count = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) ); + $query_hash = md5( $query ); + + if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { + $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); + $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) ); + set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS ); + } + + $report_data->renewal_count = $cached_results[ $query_hash ]; + + // Renewal revenue this month + $query = $wpdb->prepare( + "SELECT SUM(order_total_meta.meta_value) + FROM {$wpdb->postmeta} as order_total_meta + RIGHT JOIN + ( + SELECT DISTINCT wcorder.ID + FROM {$wpdb->posts} AS wcorder + INNER JOIN {$wpdb->postmeta} AS meta__subscription_renewal + ON ( + wcorder.id = meta__subscription_renewal.post_id + AND + meta__subscription_renewal.meta_key = '_subscription_renewal' + ) + WHERE wcorder.post_type IN ( 'shop_order' ) + AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' ) + AND wcorder.post_date >= '%s' + AND wcorder.post_date < '%s' + ) AS orders ON orders.ID = order_total_meta.post_id + WHERE order_total_meta.meta_key = '_order_total'", + date( 'Y-m-01', current_time( 'timestamp' ) ), + date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ) + ); + + $query_hash = md5( $query ); + + if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { + $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); + $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_revenue_query', $query ) ); + set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS ); + } + + $report_data->renewal_revenue = $cached_results[ $query_hash ]; + + // Cancellation count this month + $query = $wpdb->prepare( + "SELECT COUNT(DISTINCT wcsubs.ID) AS count + FROM {$wpdb->posts} AS wcsubs + JOIN {$wpdb->postmeta} AS wcsmeta_cancel + ON wcsubs.ID = wcsmeta_cancel.post_id + AND wcsmeta_cancel.meta_key = '_schedule_cancelled' + AND wcsubs.post_status NOT IN ( 'trash', 'auto-draft' ) + AND CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', '{$site_timezone}' ) BETWEEN '%s' AND '%s'", + date( 'Y-m-01', current_time( 'timestamp' ) ), + date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ) + ); + + $query_hash = md5( $query ); + + if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) { + $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); + $cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_cancellation_query', $query ) ); + set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS ); + } + + $report_data->cancel_count = $cached_results[ $query_hash ]; + + return $report_data; + } + + /** + * Add the subscription specific details to the bottom of the dashboard widget + * + * @since 2.1 + */ + public static function add_stats_to_dashboard() { + $report_data = self::get_data(); ?> +
  • - - %s renewal subscription renewals this month', '%s renewals subscription renewals this month', $renewal_count, 'woocommerce-subscriptions' ) ), esc_html( $renewal_count ) ); ?> + + renewal_count, 'woocommerce-subscriptions' ), $report_data->renewal_count, '', '' ) ); + ?> + +
  • +
  • + + ' . wc_price( $report_data->renewal_revenue ) . '' ) ); ?> + +
  • +
  • + + cancel_count, 'woocommerce-subscriptions' ), $report_data->cancel_count, '', '' ) ); ?>
  • '; echo '
    '; echo '

    ' . esc_html__( 'Customer Totals', 'woocommerce-subscriptions' ) . '

    '; - echo '

    ' . esc_html__( 'Total Subscribers', 'woocommerce-subscriptions' ) . ' : ' . esc_html( $this->totals->total_customers ) . '
    '; - echo ' ' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . ' : ' . esc_html( $this->totals->active_subscriptions ) . '
    '; - echo ' ' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . ' : ' . esc_html( $this->totals->total_subscriptions ) . '
    '; - echo ' ' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . ' : ' . esc_html( $this->totals->initial_order_count + $this->totals->renewal_switch_count ) . '
    '; - echo ' ' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . ' : ' . wp_kses_post( wc_price( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) ) . '

    '; + echo '

    ' . esc_html__( 'Total Subscribers', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->total_customers ) . wcs_help_tip( __( 'The number of unique customers with a subscription of any status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . '
    '; + echo ' ' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->active_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status of active or pending cancellation.', 'woocommerce-subscriptions' ) ) . '
    '; + echo ' ' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->total_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . '
    '; + echo ' ' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . ': ' . esc_html( $this->totals->initial_order_count + $this->totals->renewal_switch_count ) . wcs_help_tip( __( 'The total number of sign-up, switch and renewal orders placed with your store with a paid status (i.e. processing or complete).', 'woocommerce-subscriptions' ) ) . '
    '; + echo ' ' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . ': ' . wp_kses_post( wc_price( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) ) . wcs_help_tip( __( 'The average value of all customers\' sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) . '

    '; echo '
    '; $this->display(); echo '
    '; @@ -75,7 +75,7 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table { return sprintf( '%d', admin_url( 'edit.php?post_type=shop_subscription&_customer_user=' ), $user->customer_id, $user->total_subscriptions ); case 'total_subscription_order_count' : - return sprintf( '%d', admin_url( 'edit.php?post_type=shop_order&_customer_user=' ), $user->customer_id, $user->initial_order_count + $user->renewal_switch_count ); + return sprintf( '%d', admin_url( 'edit.php?post_type=shop_order&_paid_subscription_orders_for_customer_user=' ), $user->customer_id, $user->initial_order_count + $user->renewal_switch_count ); case 'customer_lifetime_value' : return wc_price( $user->initial_order_total + $user->renewal_switch_total ); diff --git a/includes/admin/reports/class-wcs-report-subscription-events-by-date.php b/includes/admin/reports/class-wcs-report-subscription-events-by-date.php index d394bc3..d159800 100755 --- a/includes/admin/reports/class-wcs-report-subscription-events-by-date.php +++ b/includes/admin/reports/class-wcs-report-subscription-events-by-date.php @@ -46,7 +46,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $query_end_date = date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ); $offset = get_option( 'gmt_offset' ); - //Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query. + // Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query. $site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 ); $this->report_data = new stdClass; @@ -371,9 +371,9 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $this->report_data->ended_counts = $cached_results[ $query_hash ]; // Total up the query data - $this->report_data->signup_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'signup_totals' ) ) ); - $this->report_data->renewal_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ) ); - $this->report_data->resubscribe_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'resubscribe_totals' ) ) ); + $this->report_data->signup_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->signup_data, 'signup_totals' ) ); + $this->report_data->renewal_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ); + $this->report_data->resubscribe_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'resubscribe_totals' ) ); $this->report_data->new_subscription_total_count = absint( array_sum( wp_list_pluck( $this->report_data->new_subscriptions, 'count' ) ) ); $this->report_data->signup_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'count' ) ) ); $this->report_data->renewal_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) ); @@ -417,7 +417,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { $legend[] = array( 'title' => sprintf( __( '%s new subscriptions', 'woocommerce-subscriptions' ), '' . $this->report_data->new_subscription_total_count . '' ), - 'placeholder' => __( 'The number of subscriptions created during this period, either by being manually created, imported or a customer placing an order.', 'woocommerce-subscriptions' ), + 'placeholder' => __( 'The number of subscriptions created during this period, either by being manually created, imported or a customer placing an order. This includes orders pending payment.', 'woocommerce-subscriptions' ), 'color' => $this->chart_colours['new_count'], 'highlight_series' => 1, ); diff --git a/includes/admin/reports/class-wcs-report-subscription-payment-retry.php b/includes/admin/reports/class-wcs-report-subscription-payment-retry.php index 235b6c9..2797764 100755 --- a/includes/admin/reports/class-wcs-report-subscription-payment-retry.php +++ b/includes/admin/reports/class-wcs-report-subscription-payment-retry.php @@ -35,23 +35,39 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report { private function query_report_data() { global $wpdb; + // Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query. + $offset = get_option( 'gmt_offset' ); + $site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 ); + $retry_date_in_local_time = $wpdb->prepare( "CONVERT_TZ(retries.date_gmt, '+00:00', %s)", $site_timezone ); + + // We need to compute this on our own since 'group_by_query' from the parent class uses posts table column names. + switch ( $this->chart_groupby ) { + case 'day': + $this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time}), DAY({$retry_date_in_local_time})"; + break; + case 'month': + $this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time})"; + break; + } + $this->report_data = new stdClass; - $query_start_date = get_gmt_from_date( date( 'Y-m-d', $this->start_date ) ); - $query_end_date = get_gmt_from_date( date( 'Y-m-d', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) ); + $query_start_date = get_gmt_from_date( date( 'Y-m-d H:i:s', $this->start_date ) ); + $query_end_date = get_gmt_from_date( date( 'Y-m-d H:i:s', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) ); - // Get the sum of order totals for completed retires (i.e. retries which eventually succeeded in processing the failed payment) + // Get the sum of order totals for completed retries (i.e. retries which eventually succeeded in processing the failed payment) $renewal_query = $wpdb->prepare( - "SELECT COUNT(DISTINCT posts.ID) as count, posts.post_date as post_date, SUM(meta_order_total.meta_value) as renewal_totals - FROM {$wpdb->prefix}posts AS orders - INNER JOIN {$wpdb->prefix}posts AS posts ON ( orders.ID = posts.post_parent ) - LEFT JOIN {$wpdb->prefix}postmeta AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' ) - WHERE posts.post_type = 'payment_retry' - AND posts.post_status = 'complete' - AND posts.post_modified_gmt >= %s - AND posts.post_modified_gmt < %s - GROUP BY {$this->group_by_query} - ORDER BY post_date ASC", + " + SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date, SUM(meta_order_total.meta_value) AS renewal_totals + FROM {$wpdb->posts} AS orders + INNER JOIN {$wpdb->prefix}wcs_payment_retries AS retries ON ( orders.ID = retries.order_id ) + LEFT JOIN {$wpdb->postmeta} AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' ) + WHERE retries.status = 'complete' + AND retries.date_gmt >= %s + AND retries.date_gmt < %s + GROUP BY {$this->group_by_query} + ORDER BY retry_date_gmt ASC + ", $query_start_date, $query_end_date ); @@ -60,14 +76,15 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report { // Get the counts for all retries, grouped by day or month and status $retry_query = $wpdb->prepare( - "SELECT COUNT(DISTINCT posts.ID) as count, posts.post_status as status, posts.post_date as post_date - FROM {$wpdb->prefix}posts AS posts - WHERE posts.post_type = 'payment_retry' - AND posts.post_status IN ( 'complete','failed','pending' ) - AND posts.post_modified_gmt >= %s - AND posts.post_modified_gmt < %s - GROUP BY {$this->group_by_query}, posts.post_status - ORDER BY posts.post_date_gmt ASC", + " + SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date + FROM {$wpdb->prefix}wcs_payment_retries AS retries + WHERE retries.status IN ( 'complete', 'failed', 'pending' ) + AND retries.date_gmt >= %s + AND retries.date_gmt < %s + GROUP BY {$this->group_by_query}, status + ORDER BY retry_date_gmt ASC + ", $query_start_date, $query_end_date ); @@ -80,7 +97,7 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report { $this->report_data->retry_pending_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'count' ) ) ); $this->report_data->renewal_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) ); - $this->report_data->renewal_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ) ); + $this->report_data->renewal_total_amount = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ); } /** @@ -188,13 +205,13 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report { global $wp_locale; // Prepare data for report - $retry_count = $this->prepare_chart_data( $this->report_data->retry_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); - $retry_success_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); - $retry_failure_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); - $retry_pending_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $retry_count = $this->prepare_chart_data( $this->report_data->retry_data, 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $retry_success_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $retry_failure_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $retry_pending_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); - $renewal_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); - $renewal_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $renewal_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); + $renewal_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'retry_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby ); // Encode in json format $chart_data = array( diff --git a/includes/api/legacy/class-wc-api-subscriptions.php b/includes/api/legacy/class-wc-api-subscriptions.php index 423901a..c5ca9f3 100755 --- a/includes/api/legacy/class-wc-api-subscriptions.php +++ b/includes/api/legacy/class-wc-api-subscriptions.php @@ -121,7 +121,7 @@ class WC_API_Subscriptions extends WC_API_Orders { public function get_subscriptions( $fields = null, $filter = array(), $status = null, $page = 1 ) { // check user permissions if ( ! current_user_can( 'read_private_shop_orders' ) ) { - return new WP_Error( 'wcs_api_user_cannot_read_susbcription_count', __( 'You do not have permission to read the subscriptions count', 'woocommerce-subscriptions' ), array( 'status' => 401 ) ); + return new WP_Error( 'wcs_api_user_cannot_read_subscription_count', __( 'You do not have permission to read the subscriptions count', 'woocommerce-subscriptions' ), array( 'status' => 401 ) ); } $status = $this->format_statuses( $status ); diff --git a/includes/class-wc-subscription.php b/includes/class-wc-subscription.php index 61dc28f..877968e 100755 --- a/includes/class-wc-subscription.php +++ b/includes/class-wc-subscription.php @@ -329,6 +329,8 @@ class WC_Subscription extends WC_Order { $can_be_updated = true; } elseif ( $this->has_status( 'pending' ) ) { $can_be_updated = true; + } elseif ( $this->has_status( 'pending-cancel' ) && ( $this->is_manual() || ( false === $this->payment_method_supports( 'gateway_scheduled_payments' ) && $this->payment_method_supports( 'subscription_date_changes' ) && $this->payment_method_supports( 'subscription_reactivation' ) ) ) ) { + $can_be_updated = true; } else { $can_be_updated = false; } @@ -450,23 +452,33 @@ class WC_Subscription extends WC_Order { case 'completed' : // core WC order status mapped internally to avoid exceptions case 'active' : - // Recalculate and set next payment date - $stored_next_payment = $this->get_time( 'next_payment' ); - // Make sure the next payment date is more than 2 hours in the future by default - if ( $stored_next_payment < ( gmdate( 'U' ) + apply_filters( 'woocommerce_subscription_activation_next_payment_date_threshold', 2 * HOUR_IN_SECONDS, $stored_next_payment, $old_status, $this ) ) ) { // also accounts for a $stored_next_payment of 0, meaning it's not set - - $calculated_next_payment = $this->calculate_date( 'next_payment' ); - - if ( $calculated_next_payment > 0 ) { - $this->update_dates( array( 'next_payment' => $calculated_next_payment ) ); - } elseif ( $stored_next_payment < gmdate( 'U' ) ) { // delete the stored date if it's in the past as we're not updating it (the calculated next payment date is 0 or none) - $this->delete_date( 'next_payment' ); - } + if ( 'pending-cancel' === $old_status ) { + $this->update_dates( array( + 'cancelled' => 0, + 'end' => 0, + 'next_payment' => $this->get_date( 'end' ), + ) ); } else { - // In case plugins want to run some code when the subscription was reactivated, but the next payment date was not recalculated. - do_action( 'woocommerce_subscription_activation_next_payment_not_recalculated', $stored_next_payment, $old_status, $this ); + // Recalculate and set next payment date + $stored_next_payment = $this->get_time( 'next_payment' ); + + // Make sure the next payment date is more than 2 hours in the future by default + if ( $stored_next_payment < ( gmdate( 'U' ) + apply_filters( 'woocommerce_subscription_activation_next_payment_date_threshold', 2 * HOUR_IN_SECONDS, $stored_next_payment, $old_status, $this ) ) ) { // also accounts for a $stored_next_payment of 0, meaning it's not set + + $calculated_next_payment = $this->calculate_date( 'next_payment' ); + + if ( $calculated_next_payment > 0 ) { + $this->update_dates( array( 'next_payment' => $calculated_next_payment ) ); + } elseif ( $stored_next_payment < gmdate( 'U' ) ) { // delete the stored date if it's in the past as we're not updating it (the calculated next payment date is 0 or none) + $this->delete_date( 'next_payment' ); + } + } else { + // In case plugins want to run some code when the subscription was reactivated, but the next payment date was not recalculated. + do_action( 'woocommerce_subscription_activation_next_payment_not_recalculated', $stored_next_payment, $old_status, $this ); + } } + // Trial end date and end/expiration date don't change at all - they should be set when the subscription is first created wcs_make_user_active( $this->get_user_id() ); break; @@ -528,33 +540,53 @@ class WC_Subscription extends WC_Order { * Handle the status transition. */ protected function status_transition() { + // Use local copy of status transition value. + $status_transition = $this->status_transition; - if ( $this->status_transition ) { - do_action( 'woocommerce_subscription_status_' . $this->status_transition['to'], $this ); + // If we're not currently in the midst of a status transition, bail early. + if ( ! $status_transition ) { + return; + } - if ( ! empty( $this->status_transition['from'] ) ) { - /* translators: 1: old subscription status 2: new subscription status */ - $transition_note = sprintf( __( 'Status changed from %1$s to %2$s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $this->status_transition['from'] ), wcs_get_subscription_status_name( $this->status_transition['to'] ) ); + try { + do_action( "woocommerce_subscription_status_{$status_transition['to']}", $this ); - do_action( 'woocommerce_subscription_status_' . $this->status_transition['from'] . '_to_' . $this->status_transition['to'], $this ); + if ( ! empty( $status_transition['from'] ) ) { + $transition_note = sprintf( + /* translators: 1: old subscription status 2: new subscription status */ + __( 'Status changed from %1$s to %2$s.', 'woocommerce-subscriptions' ), + wcs_get_subscription_status_name( $status_transition['from'] ), + wcs_get_subscription_status_name( $status_transition['to'] ) + ); - // Trigger a hook with params we want - do_action( 'woocommerce_subscription_status_updated', $this, $this->status_transition['to'], $this->status_transition['from'] ); + do_action( "woocommerce_subscription_status_{$status_transition['from']}_to_{$status_transition['to']}", $this ); - // Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes - do_action( 'woocommerce_subscription_status_changed', $this->get_id(), $this->status_transition['from'], $this->status_transition['to'], $this ); + // Trigger a hook with params we want. + do_action( 'woocommerce_subscription_status_updated', $this, $status_transition['to'], $status_transition['from'] ); + // Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes. + do_action( 'woocommerce_subscription_status_changed', $this->get_id(), $status_transition['from'], $status_transition['to'], $this ); } else { /* translators: %s: new order status */ - $transition_note = sprintf( __( 'Status set to %s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $this->status_transition['to'] ) ); + $transition_note = sprintf( __( 'Status set to %s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $status_transition['to'] ) ); } - // Note the transition occured - $this->add_order_note( trim( $this->status_transition['note'] . ' ' . $transition_note ), 0, $this->status_transition['manual'] ); - - // This has ran, so reset status transition variable - $this->status_transition = false; + // Note the transition occurred. + $this->add_order_note( trim( "{$status_transition['note']} {$transition_note}" ), 0, $status_transition['manual'] ); + } catch ( Exception $e ) { + $logger = wc_get_logger(); + $logger->error( + sprintf( 'Status transition of subscription #%d errored!', $this->get_id() ), + array( + 'order' => $this, + 'error' => $e, + ) + ); + $this->add_order_note( __( 'Error during subscription status transition.', 'woocommerce-subscriptions' ) . ' ' . $e->getMessage() ); } + + // This has run, so reset status transition variable + $this->status_transition = false; } /** @@ -1799,10 +1831,13 @@ class WC_Subscription extends WC_Order { $related_orders = array(); foreach ( $order_types as $order_type ) { - $related_orders_for_order_type = array(); foreach ( $this->get_related_order_ids( $order_type ) as $order_id ) { - $related_orders_for_order_type[ $order_id ] = ( 'all' == $return_fields ) ? wc_get_order( $order_id ) : $order_id; + if ( 'all' === $return_fields && $order = wc_get_order( $order_id ) ) { + $related_orders_for_order_type[ $order_id ] = $order; + } elseif ( 'ids' === $return_fields ) { + $related_orders_for_order_type[ $order_id ] = $order_id; + } } $related_orders += apply_filters( 'woocommerce_subscription_related_orders', $related_orders_for_order_type, $this, $return_fields, $order_type ); @@ -1886,9 +1921,11 @@ class WC_Subscription extends WC_Order { /** * Determine how the payment method should be displayed for a subscription. * + * @param string $context The context the payment method is being displayed in. Can be 'admin' or 'customer'. Default 'admin'. + * * @since 2.0 */ - public function get_payment_method_to_display() { + public function get_payment_method_to_display( $context = 'admin' ) { if ( $this->is_manual() ) { @@ -1899,14 +1936,25 @@ class WC_Subscription extends WC_Order { $payment_method_to_display = $payment_gateway->get_title(); - // Fallback to the title of the payment method when the subscripion was created + // Fallback to the title of the payment method when the subscription was created } else { $payment_method_to_display = $this->get_payment_method_title(); } - return apply_filters( 'woocommerce_subscription_payment_method_to_display', $payment_method_to_display, $this ); + $payment_method_to_display = apply_filters( 'woocommerce_subscription_payment_method_to_display', $payment_method_to_display, $this, $context ); + + if ( 'customer' === $context ) { + $payment_method_to_display = sprintf( __( 'Via %s', 'woocommerce-subscriptions' ), $payment_method_to_display ); + + // Only filter the result for non-manual subscriptions. + if ( ! $this->is_manual() ) { + $payment_method_to_display = apply_filters( 'woocommerce_my_subscriptions_payment_method', $payment_method_to_display, $this ); + } + } + + return $payment_method_to_display; } /** @@ -2029,6 +2077,16 @@ class WC_Subscription extends WC_Order { return $has_product; } + /** + * Check if the subscription has a payment gateway. + * + * @since 2.5.0 + * @return bool + */ + public function has_payment_gateway() { + return (bool) wc_get_payment_gateway_by_order( $this ); + } + /** * The total sign-up fee for the subscription if any. * @@ -2355,7 +2413,17 @@ class WC_Subscription extends WC_Order { } /** - * Get the subscription's payment method meta. + * Generates a URL to add or change the subscription's payment method from the my account page. + * + * @return string + * @since 2.5.0 + */ + public function get_change_payment_method_url() { + $change_payment_method_url = wc_get_endpoint_url( 'subscription-payment-method', $this->get_id(), wc_get_page_permalink( 'myaccount' ) ); + return apply_filters( 'wcs_get_change_payment_method_url', $change_payment_method_url, $this->get_id() ); + } + + /* Get the subscription's payment method meta. * * @since 2.4.3 * @return array The subscription's payment meta in the format returned by the woocommerce_subscription_payment_meta filter. @@ -2396,6 +2464,23 @@ class WC_Subscription extends WC_Order { return null; } + /** + * Get totals for display on pages and in emails. + * + * @param mixed $tax_display Excl or incl tax display mode. + * @return array + */ + public function get_order_item_totals( $tax_display = '' ) { + $total_rows = parent::get_order_item_totals( $tax_display ); + + // Use get_payment_method_to_display() as it displays "Manual Renewal" for manual subscriptions. + if ( isset( $total_rows['payment_method'] ) ) { + $total_rows['payment_method']['value'] = $this->get_payment_method_to_display( 'customer' ); + } + + return apply_filters( 'woocommerce_get_subscription_item_totals', $total_rows, $this, $tax_display ); + } + /************************ * Deprecated Functions * ************************/ diff --git a/includes/class-wc-subscriptions-cart.php b/includes/class-wc-subscriptions-cart.php index e1a3e82..b85c400 100755 --- a/includes/class-wc-subscriptions-cart.php +++ b/includes/class-wc-subscriptions-cart.php @@ -80,13 +80,6 @@ class WC_Subscriptions_Cart { // Make sure cart product prices correctly include/exclude taxes add_filter( 'woocommerce_cart_product_price', __CLASS__ . '::cart_product_price' , 10, 2 ); - // Make sure cart totals are calculated when setting up the cart widget - add_action( 'wc_ajax_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments' , 1 ); - add_action( 'wp_ajax_woocommerce_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments', 1 ); - add_action( 'wp_ajax_nopriv_woocommerce_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments', 1, 1 ); - - add_action( 'woocommerce_ajax_added_to_cart', __CLASS__ . '::pre_get_refreshed_fragments', 1, 1 ); - // Display grouped recurring amounts after order totals on the cart/checkout pages add_action( 'woocommerce_cart_totals_after_order_total', __CLASS__ . '::display_recurring_totals' ); add_action( 'woocommerce_review_order_after_order_total', __CLASS__ . '::display_recurring_totals' ); @@ -797,6 +790,17 @@ class WC_Subscriptions_Cart { return $cart_contains_free_trial; } + /** + * Checks to see if payment method is required on a subscription product with a $0 initial payment. + * + * @since 2.5.0 + */ + public static function zero_initial_payment_requires_payment() { + + return 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_zero_initial_payment_requires_payment', 'no' ); + + } + /** * Gets the cart calculation type flag * @@ -860,12 +864,28 @@ class WC_Subscriptions_Cart { * @return bool */ public static function cart_needs_payment( $needs_payment, $cart ) { - if ( false === $needs_payment && self::cart_contains_subscription() && $cart->total == 0 && false === WC_Subscriptions_Switcher::cart_contains_switches() && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) { - $recurring_total = 0; - $is_one_period = true; - $contains_synced = false; - $contains_expiring_limited_coupon = false; + // Skip checks if needs payment is already set or cart total not 0. + if ( false !== $needs_payment || 0 != $cart->total ) { + return $needs_payment; + } + + // Skip checks if new $0 initial payments don't require a payment method or cart has no subscriptions. + if ( ! self::zero_initial_payment_requires_payment() || ! self::cart_contains_subscription() ) { + return $needs_payment; + } + + // Skip checks if cart contains subscription switches or automatic payments are disabled. + if ( false !== WC_Subscriptions_Switcher::cart_contains_switches() || 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) { + return $needs_payment; + } + + $recurring_total = 0; + $is_one_period = true; + $contains_synced = false; + $contains_expiring_limited_coupon = false; + + if ( ! empty( WC()->cart->recurring_carts ) ) { foreach ( WC()->cart->recurring_carts as $recurring_cart ) { $recurring_total += $recurring_cart->total; $subscription_length = wcs_cart_pluck( $recurring_cart, 'subscription_length' ); @@ -876,12 +896,12 @@ class WC_Subscriptions_Cart { $is_one_period = false; } } + } - $has_trial = self::cart_contains_free_trial(); + $needs_trial_payment = self::cart_contains_free_trial(); - if ( $contains_expiring_limited_coupon || $recurring_total > 0 && ( ! $is_one_period || $has_trial || $contains_synced ) ) { - $needs_payment = true; - } + if ( $contains_expiring_limited_coupon || $recurring_total > 0 && ( ! $is_one_period || $needs_trial_payment || $contains_synced ) ) { + $needs_payment = true; } return $needs_payment; @@ -963,19 +983,6 @@ class WC_Subscriptions_Cart { return $price; } - /** - * Make sure cart totals are calculated when the cart widget is populated via the get_refreshed_fragments() method - * so that @see self::get_formatted_cart_subtotal() returns the correct subtotal price string. - * - * @since 1.5.11 - */ - public static function pre_get_refreshed_fragments() { - if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - WC()->cart->calculate_totals(); - } - } - /** * Display the recurring totals for items in the cart * @@ -1343,6 +1350,21 @@ class WC_Subscriptions_Cart { /* Deprecated */ + /** + * Make sure cart totals are calculated when the cart widget is populated via the get_refreshed_fragments() method + * so that @see self::get_formatted_cart_subtotal() returns the correct subtotal price string. + * + * @since 1.5.11 + * @deprecated 2.5.0 + */ + public static function pre_get_refreshed_fragments() { + wcs_deprecated_function( __METHOD__, '2.5.0' ); + if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + WC()->cart->calculate_totals(); + } + } + /** * Checks the cart to see if it contains a subscription product renewal. * diff --git a/includes/class-wc-subscriptions-change-payment-gateway.php b/includes/class-wc-subscriptions-change-payment-gateway.php index ff166e7..5eb9a08 100755 --- a/includes/class-wc-subscriptions-change-payment-gateway.php +++ b/includes/class-wc-subscriptions-change-payment-gateway.php @@ -67,6 +67,10 @@ class WC_Subscriptions_Change_Payment_Gateway { // Maybe filter subscriptions_needs_payment to return false when processing change-payment-gateway requests add_filter( 'woocommerce_subscription_needs_payment', __CLASS__ . '::maybe_override_needs_payment', 10, 1 ); + + // Display a login form if the customer is requesting to change their payment method but aren't logged in. + add_filter( 'the_content', array( __CLASS__, 'maybe_request_log_in' ) ); + } /** @@ -310,11 +314,16 @@ class WC_Subscriptions_Change_Payment_Gateway { if ( $subscription->can_be_updated_to( 'new-payment-method' ) ) { + if ( $subscription->has_payment_gateway() && wc_get_payment_gateway_by_order( $subscription )->supports( 'subscriptions' ) ) { + $action_name = _x( 'Change Payment', 'label on button, imperative', 'woocommerce-subscriptions' ); + } else { + $action_name = _x( 'Add Payment', 'label on button, imperative', 'woocommerce-subscriptions' ); + } + $actions['change_payment_method'] = array( 'url' => wp_nonce_url( add_query_arg( array( 'change_payment_method' => $subscription->get_id() ), $subscription->get_checkout_payment_url() ) ), - 'name' => _x( 'Change Payment', 'label on button, imperative', 'woocommerce-subscriptions' ), + 'name' => $action_name, ); - } return $actions; @@ -331,81 +340,202 @@ class WC_Subscriptions_Change_Payment_Gateway { */ public static function change_payment_method_via_pay_shortcode() { - if ( isset( $_POST['_wcsnonce'] ) && wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) ) { + if ( ! isset( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) ) { + return; + } - $subscription = wcs_get_subscription( absint( $_POST['woocommerce_change_payment'] ) ); + $subscription_id = absint( $_POST['woocommerce_change_payment'] ); + $subscription = wcs_get_subscription( $subscription_id ); - do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription ); + do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription ); - ob_start(); + ob_start(); - if ( $subscription->get_order_key() == $_GET['key'] ) { + if ( $subscription->get_order_key() == $_GET['key'] ) { - $subscription_billing_country = $subscription->get_billing_country(); - $subscription_billing_state = $subscription->get_billing_state(); - $subscription_billing_postcode = $subscription->get_billing_postcode(); - $subscription_billing_city = $subscription->get_billing_postcode(); + $subscription_billing_country = $subscription->get_billing_country(); + $subscription_billing_state = $subscription->get_billing_state(); + $subscription_billing_postcode = $subscription->get_billing_postcode(); + $subscription_billing_city = $subscription->get_billing_postcode(); - // Set customer location to order location - if ( $subscription_billing_country ) { - $setter = is_callable( array( WC()->customer, 'set_billing_country' ) ) ? 'set_billing_country' : 'set_country'; - WC()->customer->$setter( $subscription_billing_country ); - } - if ( $subscription_billing_state ) { - $setter = is_callable( array( WC()->customer, 'set_billing_state' ) ) ? 'set_billing_state' : 'set_state'; - WC()->customer->$setter( $subscription_billing_state ); - } - if ( $subscription_billing_postcode ) { - $setter = is_callable( array( WC()->customer, 'set_billing_postcode' ) ) ? 'set_billing_postcode' : 'set_postcode'; - WC()->customer->$setter( $subscription_billing_postcode ); - } - if ( $subscription_billing_city ) { - $setter = is_callable( array( WC()->customer, 'set_billing_city' ) ) ? 'set_billing_city' : 'set_city'; - WC()->customer->$setter( $subscription_billing_city ); + // Set customer location to order location + if ( $subscription_billing_country ) { + $setter = is_callable( array( WC()->customer, 'set_billing_country' ) ) ? 'set_billing_country' : 'set_country'; + WC()->customer->$setter( $subscription_billing_country ); + } + if ( $subscription_billing_state ) { + $setter = is_callable( array( WC()->customer, 'set_billing_state' ) ) ? 'set_billing_state' : 'set_state'; + WC()->customer->$setter( $subscription_billing_state ); + } + if ( $subscription_billing_postcode ) { + $setter = is_callable( array( WC()->customer, 'set_billing_postcode' ) ) ? 'set_billing_postcode' : 'set_postcode'; + WC()->customer->$setter( $subscription_billing_postcode ); + } + if ( $subscription_billing_city ) { + $setter = is_callable( array( WC()->customer, 'set_billing_city' ) ) ? 'set_billing_city' : 'set_city'; + WC()->customer->$setter( $subscription_billing_city ); + } + + // Update payment method + $new_payment_method = wc_clean( $_POST['payment_method'] ); + $notice = $subscription->has_payment_gateway() ? __( 'Payment method updated.', 'woocommerce-subscriptions' ) : __( 'Payment method added.', 'woocommerce-subscriptions' ); + + // Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed + if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) { + self::update_payment_method( $subscription, $new_payment_method ); + } + + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + + // Validate + $available_gateways[ $new_payment_method ]->validate_fields(); + + // Process payment for the new method (with a $0 order total) + if ( wc_notice_count( 'error' ) == 0 ) { + + $result = $available_gateways[ $new_payment_method ]->process_payment( $subscription->get_id() ); + + if ( 'success' == $result['result'] && wc_get_page_permalink( 'myaccount' ) == $result['redirect'] ) { + $result['redirect'] = $subscription->get_view_order_url(); } - // Update payment method - $new_payment_method = wc_clean( $_POST['payment_method'] ); + $result = apply_filters( 'woocommerce_subscriptions_process_payment_for_change_method_via_pay_shortcode', $result, $subscription ); - // Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed - if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) { - self::update_payment_method( $subscription, $new_payment_method ); + if ( 'success' != $result['result'] ) { + return; } - $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $subscription->set_requires_manual_renewal( false ); + $subscription->save(); - // Validate - $available_gateways[ $new_payment_method ]->validate_fields(); - - // Process payment for the new method (with a $0 order total) - if ( wc_notice_count( 'error' ) == 0 ) { - - $result = $available_gateways[ $new_payment_method ]->process_payment( $subscription->get_id() ); - - if ( 'success' == $result['result'] && wc_get_page_permalink( 'myaccount' ) == $result['redirect'] ) { - $result['redirect'] = $subscription->get_view_order_url(); - } - - $result = apply_filters( 'woocommerce_subscriptions_process_payment_for_change_method_via_pay_shortcode', $result, $subscription ); - - // Redirect to success/confirmation/payment page - if ( 'success' == $result['result'] ) { - wc_add_notice( __( 'Payment method updated.', 'woocommerce-subscriptions' ), 'success' ); - wp_redirect( $result['redirect'] ); - exit; + // Does the customer want all current subscriptions to be updated to this payment method? + if ( + isset( $_POST['update_all_subscriptions_payment_method'] ) + && $_POST['update_all_subscriptions_payment_method'] + && WC_Subscriptions_Change_Payment_Gateway::can_update_all_subscription_payment_methods( $available_gateways[ $new_payment_method ], $subscription ) + ) { + // Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed + if ( ! apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) { + $subscription->update_meta_data( '_delayed_update_payment_method_all', $new_payment_method ); + $subscription->save(); + $notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-subscriptions' ); + } elseif ( self::update_all_payment_methods_from_subscription( $subscription, $new_payment_method ) ) { + $notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-subscriptions' ); } } + + // Redirect to success/confirmation/payment page + wc_add_notice( $notice ); + wp_redirect( $result['redirect'] ); + exit; } } + + ob_get_clean(); + } + + /** + * Update the recurring payment method on all current subscriptions to the payment method on this subscription. + * + * @param WC_Subscription $subscription An instance of a WC_Subscription object. + * @param string $new_payment_method The ID of the new payment method. + * @return bool Were other subscriptions updated. + * @since 2.5.0 + */ + public static function update_all_payment_methods_from_subscription( $subscription, $new_payment_method ) { + + // Require the delayed payment update method to match the current gateway if it is set + if ( self::will_subscription_update_all_payment_methods( $subscription ) ) { + if ( $subscription->get_meta( '_delayed_update_payment_method_all' ) != $new_payment_method ) { + return false; + } + + $subscription->delete_meta_data( '_delayed_update_payment_method_all' ); + $subscription->save_meta_data(); + } + + $payment_meta_table = WCS_Payment_tokens::get_subscription_payment_meta( $subscription, $new_payment_method ); + if ( ! is_array( $payment_meta_table ) ) { + return false; + } + + $subscription_ids = WCS_Customer_Store::instance()->get_users_subscription_ids( $subscription->get_customer_id() ); + + foreach ( $subscription_ids as $subscription_id ) { + // Skip the subscription providing the new payment meta. + if ( $subscription->get_id() == $subscription_id ) { + continue; + } + + $user_subscription = wcs_get_subscription( $subscription_id ); + // Skip if subscription's current payment method is not supported + if ( ! $user_subscription->payment_method_supports( 'subscription_cancellation' ) ) { + continue; + } + + // Skip if there are no remaining payments or the subscription is not current. + if ( $user_subscription->get_time( 'next_payment' ) <= 0 || ! $user_subscription->has_status( array( 'active', 'on-hold' ) ) ) { + continue; + } + + self::update_payment_method( $user_subscription, $new_payment_method, $payment_meta_table ); + + $user_subscription->set_requires_manual_renewal( false ); + $user_subscription->save(); + } + + return true; + } + + /** + * Check whether a payment method supports updating all current subscriptions' payment method. + * + * @param WC_Payment_Gateway $gateway The payment gateway to check. + * @param WC_Subscription $subscription An instance of a WC_Subscription object. + * @return bool Gateway supports updating all current subscriptions. + * @since 2.5.0 + */ + public static function can_update_all_subscription_payment_methods( $gateway, $subscription ) { + + if ( $gateway->supports( 'subscription_payment_method_delayed_change' ) ) { + return true; + } + + if ( ! $gateway->supports( 'subscription_payment_method_change_admin' ) ) { + return false; + } + + if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $gateway->id, $subscription ) ) { + return true; + } + + return false; + } + + /** + * Check whether a subscription will update all current subscriptions' payment method. + * + * @param WC_Subscription $subscription An instance of a WC_Subscription object. + * @return bool Subscription will update all current subscriptions. + * @since 2.5.0 + */ + public static function will_subscription_update_all_payment_methods( $subscription ) { + if ( ! wcs_is_subscription( $subscription ) ) { + return false; + } + + return (bool) $subscription->get_meta( '_delayed_update_payment_method_all' ); } /** * Update the recurring payment method on a subscription order. * - * @param array $available_gateways The payment gateways which are currently being allowed. + * @param WC_Subscription $subscription An instance of a WC_Subscription object. + * @param string $new_payment_method The ID of the new payment method. + * @param array $new_payment_method_meta The meta for the new payment method. Optional. Default false. * @since 1.4 */ - public static function update_payment_method( $subscription, $new_payment_method ) { + public static function update_payment_method( $subscription, $new_payment_method, $new_payment_method_meta = false ) { $old_payment_method = $subscription->get_payment_method(); $old_payment_method_title = $subscription->get_payment_method_title(); @@ -417,18 +547,12 @@ class WC_Subscriptions_Change_Payment_Gateway { WC_Subscriptions_Payment_Gateways::trigger_gateway_status_updated_hook( $subscription, 'cancelled' ); // Update meta - update_post_meta( $subscription->get_id(), '_old_payment_method', $old_payment_method ); - update_post_meta( $subscription->get_id(), '_payment_method', $new_payment_method ); - if ( isset( $available_gateways[ $new_payment_method ] ) ) { $new_payment_method_title = $available_gateways[ $new_payment_method ]->get_title(); } else { $new_payment_method_title = ''; } - update_post_meta( $subscription->get_id(), '_old_payment_method_title', $old_payment_method_title ); - update_post_meta( $subscription->get_id(), '_payment_method_title', $new_payment_method_title ); - if ( empty( $old_payment_method_title ) ) { $old_payment_method_title = $old_payment_method; } @@ -437,12 +561,21 @@ class WC_Subscriptions_Change_Payment_Gateway { $new_payment_method_title = $new_payment_method; } + $subscription->update_meta_data( '_old_payment_method', $old_payment_method ); + $subscription->update_meta_data( '_old_payment_method_title', $old_payment_method_title ); + $subscription->set_payment_method( $new_payment_method, $new_payment_method_meta ); + $subscription->set_payment_method_title( $new_payment_method_title ); + // Log change on order $subscription->add_order_note( sprintf( _x( 'Payment method changed from "%1$s" to "%2$s" by the subscriber from their account page.', '%1$s: old payment title, %2$s: new payment title', 'woocommerce-subscriptions' ), $old_payment_method_title, $new_payment_method_title ) ); + $subscription->save(); + do_action( 'woocommerce_subscription_payment_method_updated', $subscription, $new_payment_method, $old_payment_method ); do_action( 'woocommerce_subscription_payment_method_updated_to_' . $new_payment_method, $subscription, $old_payment_method ); - do_action( 'woocommerce_subscription_payment_method_updated_from_' . $old_payment_method, $subscription, $new_payment_method ); + if ( $old_payment_method ) { + do_action( 'woocommerce_subscription_payment_method_updated_from_' . $old_payment_method, $subscription, $new_payment_method ); + } } /** @@ -552,26 +685,39 @@ class WC_Subscriptions_Change_Payment_Gateway { * For the recurring payment method to be changeable, the subscription must be active, have future (automatic) payments * and use a payment gateway which allows the subscription to be cancelled. * - * @param bool $subscription_can_be_changed Flag of whether the subscription can be changed to - * @param string $new_status_or_meta The status or meta data you want to change th subscription to. Can be 'active', 'on-hold', 'cancelled', 'expired', 'trash', 'deleted', 'failed', 'new-payment-date' or some other value attached to the 'woocommerce_can_subscription_be_changed_to' filter. - * @param object $args Set of values used in @see WC_Subscriptions_Manager::can_subscription_be_changed_to() for determining if a subscription can be changes, include: - * 'subscription_key' string A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key() - * 'subscription' array Subscription of the form returned by @see WC_Subscriptions_Manager::get_subscription() - * 'user_id' int The ID of the subscriber. - * 'order' WC_Order The order which recorded the successful payment (to make up for the failed automatic payment). - * 'payment_gateway' WC_Payment_Gateway The subscription's recurring payment gateway - * 'order_uses_manual_payments' bool A boolean flag indicating whether the subscription requires manual renewal payment. + * @param bool $subscription_can_be_changed Flag of whether the subscription can be changed. + * @param WC_Subscription $subscription The subscription to check. + * @return bool Flag indicating whether the subscription payment method can be updated. * @since 1.4 */ public static function can_subscription_be_updated_to_new_payment_method( $subscription_can_be_changed, $subscription ) { - if ( WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_customer' ) && $subscription->get_time( 'next_payment' ) > 0 && ! $subscription->is_manual() && $subscription->payment_method_supports( 'subscription_cancellation' ) && $subscription->has_status( 'active' ) ) { - $subscription_can_be_changed = true; - } else { - $subscription_can_be_changed = false; + // Don't allow if automatic payments are disabled and the toggle is also disabled. + if ( 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) && ! WCS_My_Account_Auto_Renew_Toggle::is_enabled() ) { + return false; } - return $subscription_can_be_changed; + // If there's no recurring payment, there's no need to add or update the payment method. + if ( $subscription->get_total() == 0 ) { + return false; + } + + // Don't allow if no gateways support changing methods. + if ( ! WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_customer' ) ) { + return false; + } + + // Don't allow if there are no remaining payments or the subscription is not active. + if ( $subscription->get_time( 'next_payment' ) <= 0 || ! $subscription->has_status( 'active' ) ) { + return false; + } + + // Don't allow on subscription that doesn't support changing methods. + if ( ! $subscription->payment_method_supports( 'subscription_cancellation' ) ) { + return false; + } + + return true; } /** @@ -583,8 +729,22 @@ class WC_Subscriptions_Change_Payment_Gateway { */ public static function change_payment_method_page_title( $title ) { - if ( is_main_query() && in_the_loop() && is_page() && is_checkout_pay_page() && self::$is_request_to_change_payment ) { + global $wp; + + // Skip if not on checkout pay page or not a payment change request. + if ( ! self::$is_request_to_change_payment || ! is_main_query() || ! in_the_loop() || ! is_page() || ! is_checkout_pay_page() ) { + return $title; + } + + $subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) ); + if ( ! $subscription ) { + return $title; + } + + if ( $subscription->has_payment_gateway() ) { $title = _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ); + } else { + $title = _x( 'Add Payment Method', 'the page title of the add payment method form', 'woocommerce-subscriptions' ); } return $title; @@ -617,10 +777,17 @@ class WC_Subscriptions_Change_Payment_Gateway { esc_url( $subscription->get_view_order_url() ), ); - $crumbs[3] = array( - _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ), - '', - ); + if ( $subscription->has_payment_gateway() ) { + $crumbs[3] = array( + _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ), + '', + ); + } else { + $crumbs[3] = array( + _x( 'Add Payment Method', 'the page title of the add payment method form', 'woocommerce-subscriptions' ), + '', + ); + } } return $crumbs; @@ -645,6 +812,37 @@ class WC_Subscriptions_Change_Payment_Gateway { return $needs_payment; } + /** + * Display a login form on the change payment method page if the customer isn't logged in. + * + * @param string $content The default HTML page content. + * @return string $content. + * @since 2.5.0 + */ + public static function maybe_request_log_in( $content ) { + global $wp; + + if ( ! self::$is_request_to_change_payment || is_user_logged_in() || ! isset( $wp->query_vars['order-pay'] ) ) { + return $content; + } + + $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(); + } + + return $content; + } + /** Deprecated Functions **/ /** diff --git a/includes/class-wc-subscriptions-checkout.php b/includes/class-wc-subscriptions-checkout.php index 51c5f6f..79ea7f4 100755 --- a/includes/class-wc-subscriptions-checkout.php +++ b/includes/class-wc-subscriptions-checkout.php @@ -48,10 +48,10 @@ class WC_Subscriptions_Checkout { public static function attach_dependant_hooks() { // Make sure guest checkout is not enabled in option param passed to WC JS if ( WC_Subscriptions::is_woocommerce_pre( '3.3' ) ) { - add_filter( 'woocommerce_params', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 1 ); - add_filter( 'wc_checkout_params', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 1 ); + add_filter( 'woocommerce_params', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 1 ); + add_filter( 'wc_checkout_params', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 1 ); } else { - add_filter( 'woocommerce_get_script_data', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 2 ); + add_filter( 'woocommerce_get_script_data', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 2 ); } } @@ -455,11 +455,18 @@ class WC_Subscriptions_Checkout { * Also make sure the guest checkout option value passed to the woocommerce.js forces registration. * Otherwise the registration form is hidden by woocommerce.js. * - * @since 1.1 + * @param string $handle Default empty string (''). + * @param array $woocommerce_params + * + * @since 2.5.3 + * @return array */ - public static function filter_woocommerce_script_paramaters( $woocommerce_params, $handle = '' ) { + public static function filter_woocommerce_script_parameters( $woocommerce_params, $handle = '' ) { // WC 3.3+ deprecates handle-specific filters in favor of 'woocommerce_get_script_data'. - if ( 'woocommerce_get_script_data' === current_filter() && ! in_array( $handle, array( 'woocommerce', 'wc-checkout' ) ) ) { + if ( 'woocommerce_get_script_data' === current_filter() && ! in_array( $handle, array( + 'woocommerce', + 'wc-checkout', + ) ) ) { return $woocommerce_params; } @@ -470,6 +477,19 @@ class WC_Subscriptions_Checkout { return $woocommerce_params; } + /** + * Also make sure the guest checkout option value passed to the woocommerce.js forces registration. + * Otherwise the registration form is hidden by woocommerce.js. + * + * @since 1.1 + * @deprecated 2.5.3 + */ + public static function filter_woocommerce_script_paramaters( $woocommerce_params, $handle = '' ) { + wcs_deprecated_function( __METHOD__, '2.5.3', 'WC_Subscriptions_Admin::filter_woocommerce_script_parameters( $woocommerce_params, $handle )' ); + + return self::filter_woocommerce_script_parameters( $woocommerce_params, $handle ); + } + /** * During the checkout process, force registration when the cart contains a subscription. * diff --git a/includes/class-wc-subscriptions-manager.php b/includes/class-wc-subscriptions-manager.php index 86e216d..9fbeda1 100755 --- a/includes/class-wc-subscriptions-manager.php +++ b/includes/class-wc-subscriptions-manager.php @@ -1195,7 +1195,7 @@ class WC_Subscriptions_Manager { } /** - * Returns the number of completed payments for a given subscription (including the intial payment). + * Returns the number of completed payments for a given subscription (including the initial payment). * * @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key() * @param int $user_id The ID of the user who owns the subscriptions. Although this parameter is optional, if you have the User ID you should pass it to improve performance. diff --git a/includes/class-wc-subscriptions-product.php b/includes/class-wc-subscriptions-product.php index b12dd56..83160f4 100755 --- a/includes/class-wc-subscriptions-product.php +++ b/includes/class-wc-subscriptions-product.php @@ -409,7 +409,9 @@ class WC_Subscriptions_Product { $sale_price = self::get_sale_price( $product ); $active_price = ( $subscription_price ) ? $subscription_price : self::get_regular_price( $product ); - if ( $product->is_on_sale() && $subscription_price > $sale_price ) { + // Ensure that $sale_price is non-empty because other plugins can use woocommerce_product_is_on_sale filter to + // forcefully set a product's is_on_sale flag (like Dynamic Pricing ) + if ( $product->is_on_sale() && '' !== $sale_price && $subscription_price > $sale_price ) { $active_price = $sale_price; } @@ -777,11 +779,13 @@ class WC_Subscriptions_Product { * @since 2.2.0 */ public static function needs_one_time_shipping( $product ) { - $product = self::maybe_get_product_instance( $product ); + $product = self::maybe_get_product_instance( $product ); + $variation = null; if ( $product && $product->is_type( 'variation' ) && is_callable( array( $product, 'get_parent_id' ) ) ) { - $product = self::maybe_get_product_instance( $product->get_parent_id() ); + $variation = $product; + $product = self::maybe_get_product_instance( $product->get_parent_id() ); } - return apply_filters( 'woocommerce_subscriptions_product_needs_one_time_shipping', 'yes' === self::get_meta_data( $product, 'subscription_one_time_shipping', 'no' ), $product ); + return apply_filters( 'woocommerce_subscriptions_product_needs_one_time_shipping', 'yes' === self::get_meta_data( $product, 'subscription_one_time_shipping', 'no' ), $product, $variation ); } /** diff --git a/includes/class-wc-subscriptions-switcher.php b/includes/class-wc-subscriptions-switcher.php index 7cd7fff..50d416c 100755 --- a/includes/class-wc-subscriptions-switcher.php +++ b/includes/class-wc-subscriptions-switcher.php @@ -1771,7 +1771,7 @@ class WC_Subscriptions_Switcher { * @since 2.0 */ public static function remove_print_switch_link() { - remove_filter( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10 ); + remove_action( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10 ); } /** @@ -1780,7 +1780,7 @@ class WC_Subscriptions_Switcher { * @since 2.0 */ public static function add_print_switch_link( $table_content ) { - add_filter( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10, 3 ); + add_action( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10, 3 ); return $table_content; } diff --git a/includes/class-wc-subscriptions-synchroniser.php b/includes/class-wc-subscriptions-synchroniser.php index 6d6b668..94f728d 100755 --- a/includes/class-wc-subscriptions-synchroniser.php +++ b/includes/class-wc-subscriptions-synchroniser.php @@ -919,7 +919,10 @@ class WC_Subscriptions_Synchroniser { add_filter( 'woocommerce_subscriptions_product_trial_expiration_date', __METHOD__, 10, 2 ); // avoid infinite loop // First make sure the day is in the past so that we don't end up jumping a month or year because of a few hours difference between now and the billing date - if ( $trial_expiration_timestamp > $first_payment_timestamp && gmdate( 'Ymd', $first_payment_timestamp ) == gmdate( 'Ymd', $trial_expiration_timestamp ) ) { + // Use site time to check if the trial expiration and first payment fall on the same day + $site_offset = get_option( 'gmt_offset' ) * 3600; + + if ( $trial_expiration_timestamp > $first_payment_timestamp && gmdate( 'Ymd', $first_payment_timestamp + $site_offset ) === gmdate( 'Ymd', $trial_expiration_timestamp + $site_offset ) ) { $trial_expiration_date = date( 'Y-m-d H:i:s', $first_payment_timestamp ); } } @@ -1097,9 +1100,9 @@ class WC_Subscriptions_Synchroniser { $subscription = wcs_get_subscription( $post_id ); foreach ( $subscription->get_items() as $item ) { - $product_id = wcs_get_canonical_product_id( $item ); + $product = $item->get_product(); - if ( self::is_product_synced( $product_id ) ) { + if ( self::is_product_synced( $product ) ) { update_post_meta( $subscription->get_id(), '_contains_synced_subscription', 'true' ); break; } @@ -1182,10 +1185,10 @@ class WC_Subscriptions_Synchroniser { * @since 2.2.3 */ public static function maybe_add_meta_for_new_line_item( $item_id, $item, $subscription_id ) { - if ( is_callable( array( $item, 'get_product_id' ) ) ) { - $product_id = wcs_get_canonical_product_id( $item ); + if ( is_callable( array( $item, 'get_product' ) ) ) { + $product = $item->get_product(); - if ( self::is_product_synced( $product_id ) ) { + if ( self::is_product_synced( $product ) ) { self::maybe_add_subscription_meta( $subscription_id ); } } diff --git a/includes/class-wcs-action-scheduler.php b/includes/class-wcs-action-scheduler.php index 9ebd1da..fef27de 100755 --- a/includes/class-wcs-action-scheduler.php +++ b/includes/class-wcs-action-scheduler.php @@ -73,6 +73,8 @@ class WCS_Action_Scheduler extends WCS_Scheduler { switch ( $new_status ) { case 'active' : + $this->unschedule_actions( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $this->get_action_args( 'end', $subscription ) ); + foreach ( $this->action_hooks as $action_hook => $date_type ) { $event_time = $subscription->get_time( $date_type ); @@ -94,6 +96,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler { as_schedule_single_action( $event_time, $action_hook, $action_args ); } } + break; case 'pending-cancel' : diff --git a/includes/class-wcs-cart-renewal.php b/includes/class-wcs-cart-renewal.php index 6af40bb..64a1d35 100755 --- a/includes/class-wcs-cart-renewal.php +++ b/includes/class-wcs-cart-renewal.php @@ -941,7 +941,15 @@ class WCS_Cart_Renewal { */ protected function set_cart_hash( $order_id ) { $order = wc_get_order( $order_id ); - wcs_set_objects_property( $order, 'cart_hash', md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total ) ); + + // Use cart hash generator introduced in WooCommerce 3.6 + if ( is_callable( array( WC()->cart, 'get_cart_hash' ) ) ) { + $cart_hash = WC()->cart->get_cart_hash(); + } else { + $cart_hash = md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total ); + } + + wcs_set_objects_property( $order, 'cart_hash', $cart_hash ); } /** @@ -1361,8 +1369,10 @@ class WCS_Cart_Renewal { protected function apply_order_coupon( $order, $coupon ) { $coupon_code = $coupon->get_code(); - // Set order products as the product ids on the coupon - $coupon->set_product_ids( $this->get_products( $order ) ); + // Set order products as the product ids on the coupon if the coupon does not already have usage restrictions for some products + if ( ! $coupon->get_product_ids() ) { + $coupon->set_product_ids( $this->get_products( $order ) ); + } // Store the coupon info for later $this->store_coupon( $order->get_id(), $coupon ); diff --git a/includes/class-wcs-failed-scheduled-action-manager.php b/includes/class-wcs-failed-scheduled-action-manager.php index d6748ef..294fcdd 100755 --- a/includes/class-wcs-failed-scheduled-action-manager.php +++ b/includes/class-wcs-failed-scheduled-action-manager.php @@ -131,13 +131,13 @@ class WCS_Failed_Scheduled_Action_Manager { ) ); $notice->set_actions( array( array( - 'name' => __( 'Ignore this error (not recommended)', 'woocommerce-subscriptions' ), + 'name' => __( 'Ignore this error', 'woocommerce-subscriptions' ), 'url' => wp_nonce_url( add_query_arg( 'wcs_scheduled_action_timeout_error_notice', 'ignore' ), 'wcs_scheduled_action_timeout_error_notice', '_wcsnonce' ), 'class' => 'button', ), array( - 'name' => __( 'Open a ticket', 'woocommerce-subscriptions' ), - 'url' => 'https://woocommerce.com/my-account/marketplace-ticket-form/', + 'name' => __( 'Learn more', 'woocommerce-subscriptions' ), + 'url' => 'https://docs.woocommerce.com/document/subscriptions/scheduled-action-errors/', 'class' => 'button button-primary', ), ) ); diff --git a/includes/class-wcs-my-account-auto-renew-toggle.php b/includes/class-wcs-my-account-auto-renew-toggle.php new file mode 100755 index 0000000..9e4bb3f --- /dev/null +++ b/includes/class-wcs-my-account-auto-renew-toggle.php @@ -0,0 +1,151 @@ +has_status( 'active' ) ) { + return false; + } + // Cannot change to auto-renewal for a subscription with 0 total + if ( 0 == $subscription->get_total() ) { // Not using strict comparison intentionally + return false; + } + // Cannot change to auto-renewal for a subscription in the final billing period. No next renewal date. + if ( 0 == $subscription->get_date( 'next_payment' ) ) { // Not using strict comparison intentionally + return false; + } + // If it is not a manual subscription, and the payment gateway is PayPal Standard + if ( ! $subscription->is_manual() && $subscription->payment_method_supports( 'gateway_scheduled_payments' ) ) { + return false; + } + + // Looks like changing to auto-renewal is indeed possible + return true; + } + + /** + * Disable auto renewal of subscription + * + * @since 2.5.0 + */ + public static function disable_auto_renew() { + + if ( ! isset( $_POST['subscription_id'] ) ) { + return -1; + } + + $subscription_id = absint( $_POST['subscription_id'] ); + check_ajax_referer( "toggle-auto-renew-{$subscription_id}", 'security' ); + + $subscription = wcs_get_subscription( $subscription_id ); + + if ( $subscription ) { + $subscription->set_requires_manual_renewal( true ); + $subscription->save(); + + self::send_ajax_response( $subscription ); + } + } + + /** + * Enable auto renewal of subscription + * + * @since 2.5.0 + */ + public static function enable_auto_renew() { + + if ( ! isset( $_POST['subscription_id'] ) ) { + return -1; + } + + $subscription_id = absint( $_POST['subscription_id'] ); + check_ajax_referer( "toggle-auto-renew-{$subscription_id}", 'security' ); + + $subscription = wcs_get_subscription( $subscription_id ); + + if ( wc_get_payment_gateway_by_order( $subscription ) ) { + $subscription->set_requires_manual_renewal( false ); + $subscription->save(); + + self::send_ajax_response( $subscription ); + } + } + + /** + * Send a response after processing the AJAX request so the page can be updated. + * + * @param WC_Subscription $subscription + */ + protected static function send_ajax_response( $subscription ) { + wp_send_json( array( 'payment_method' => esc_attr( $subscription->get_payment_method_to_display( 'customer' ) ) ) ); + } + + /** + * Add a setting to allow store managers to enable or disable the auto-renewal toggle. + * + * @param array $settings + * @return array + * @since 2.5.0 + */ + public static function add_setting( $settings ) { + WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_subscriptions_turn_off_automatic_payments', array( + 'id' => self::$setting_id, + 'name' => __( 'Auto Renewal Toggle', 'woocommerce-subscriptions' ), + 'desc' => __( 'Display the auto renewal toggle', 'woocommerce-subscriptions' ), + 'desc_tip' => __( 'Allow customers to turn on and off automatic renewals from their View Subscription page.', 'woocommerce-subscriptions' ), + 'default' => 'no', + 'type' => 'checkbox', + ) ); + + return $settings; + } + + /** + * Checks if the store has enabled the auto-renewal toggle. + * + * @return bool true if the toggle is enabled, otherwise false. + * @since 2.5.0 + */ + public static function is_enabled() { + return 'yes' === get_option( self::$setting_id, 'no' ); + } +} diff --git a/includes/class-wcs-my-account-payment-methods.php b/includes/class-wcs-my-account-payment-methods.php index 2c4a815..14b6615 100755 --- a/includes/class-wcs-my-account-payment-methods.php +++ b/includes/class-wcs-my-account-payment-methods.php @@ -9,9 +9,6 @@ */ class WCS_My_Account_Payment_Methods { - /* A cache of a customer's payment tokens to avoid running multiple queries in the same request */ - protected static $customer_tokens = array(); - /** * Initialize filters and hooks for class. * @@ -27,6 +24,7 @@ class WCS_My_Account_Payment_Methods { add_action( 'woocommerce_payment_token_deleted',array( __CLASS__, 'maybe_update_subscriptions_payment_meta' ), 10, 2 ); add_action( 'woocommerce_payment_token_set_default', array( __CLASS__, 'display_default_payment_token_change_notice' ), 10, 2 ); add_action( 'wp', array( __CLASS__, 'update_subscription_tokens' ) ); + } /** @@ -41,8 +39,8 @@ class WCS_My_Account_Payment_Methods { if ( $payment_token instanceof WC_Payment_Token && isset( $payment_token_data['actions']['delete']['url'] ) ) { - if ( 0 < count( self::get_subscriptions_by_token( $payment_token ) ) ) { - if ( self::customer_has_alternative_token( $payment_token ) ) { + if ( 0 < count( WCS_Payment_Tokens::get_subscriptions_from_token( $payment_token ) ) ) { + if ( WCS_Payment_Tokens::customer_has_alternative_token( $payment_token ) ) { $delete_subscription_token_args = array( 'delete_subscription_token' => $payment_token->get_id(), 'wcs_nonce' => wp_create_nonce( 'delete_subscription_token_' . $payment_token->get_id() ), @@ -76,7 +74,7 @@ class WCS_My_Account_Payment_Methods { // init payment gateways WC()->payment_gateways(); - $new_token = self::get_customers_alternative_token( $deleted_token ); + $new_token = WCS_Payment_Tokens::get_customers_alternative_token( $deleted_token ); if ( empty( $new_token ) ) { $notice = esc_html__( 'The deleted payment method was used for automatic subscription payments, we couldn\'t find an alternative token payment method token to change your subscriptions to.', 'woocommerce-subscriptions' ); @@ -84,20 +82,18 @@ class WCS_My_Account_Payment_Methods { return; } - $subscriptions = self::get_subscriptions_by_token( $deleted_token ); + $subscriptions = WCS_Payment_Tokens::get_subscriptions_from_token( $deleted_token ); if ( empty( $subscriptions ) ) { return; } foreach ( $subscriptions as $subscription ) { - $subscription = wcs_get_subscription( $subscription ); - if ( empty( $subscription ) ) { continue; } - if ( self::update_subscription_token( $subscription, $new_token, $deleted_token ) ) { + if ( WCS_Payment_Tokens::update_subscription_token( $subscription, $new_token, $deleted_token ) ) { $subscription->add_order_note( sprintf( _x( 'Payment method meta updated after customer deleted a token from their My Account page. Payment meta changed from %1$s to %2$s', 'used in subscription note', 'woocommerce-subscriptions' ), $deleted_token->get_token(), $new_token->get_token() ) ); } } @@ -112,75 +108,6 @@ class WCS_My_Account_Payment_Methods { wc_add_notice( $notice , 'notice' ); } - /** - * Update the subscription payment meta to change from an old payment token to a new one. - * - * @param WC_Subscription $subscription The subscription to update. - * @param WC_Payment_Token $new_token The new payment token. - * @param WC_Payment_Token $old_token The old payment token. - * @return bool Whether the subscription was updated or not. - */ - protected static function update_subscription_token( $subscription, $new_token, $old_token ) { - $payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription ); - $token_payment_gateway = $old_token->get_gateway_id(); - $token_meta_key = ''; - - // Attempt to find the token meta key from the subscription payment meta and the old token. - if ( is_array( $payment_method_meta ) && isset( $payment_method_meta[ $token_payment_gateway ] ) && is_array( $payment_method_meta[ $token_payment_gateway ] ) ) { - foreach ( $payment_method_meta[ $token_payment_gateway ] as $meta_table => $meta ) { - foreach ( $meta as $meta_key => $meta_data ) { - if ( $old_token->get_token() === $meta_data['value'] ) { - $token_meta_key = $meta_key; - break 2; - } - } - } - } - - $updated = update_post_meta( $subscription->get_id(), $token_meta_key, $new_token->get_token(), $old_token->get_token() ); - - if ( $updated ) { - do_action( 'woocommerce_subscription_token_changed', $subscription, $new_token, $old_token ); - } - - return $updated; - } - - /** - * Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method, - * customer id and token value stored in post meta will be returned. - * - * @param WC_Payment_Token payment token object - * @return array subscription posts - * @since 2.2.7 - */ - public static function get_subscriptions_by_token( $payment_token ) { - - $meta_query = array( - array( - 'key' => '_payment_method', - 'value' => $payment_token->get_gateway_id(), - ), - array( - 'key' => '_requires_manual_renewal', - 'value' => 'false', - ), - array( - 'value' => $payment_token->get_token(), - ), - ); - - $user_subscriptions = get_posts( array( - 'post_type' => 'shop_subscription', - 'post_status' => array( 'wc-pending', 'wc-active', 'wc-on-hold' ), - 'meta_query' => $meta_query, - 'posts_per_page' => -1, - 'post__in' => WCS_Customer_Store::instance()->get_users_subscription_ids( $payment_token->get_user_id() ), - ) ); - - return apply_filters( 'woocommerce_subscriptions_by_payment_token', $user_subscriptions, $payment_token ); - } - /** * Get a WC_Payment_Token label. eg Visa ending in 1234 * @@ -199,66 +126,6 @@ class WCS_My_Account_Payment_Methods { return $label; } - /** - * Get a list of customer payment tokens. Caches results to avoid multiple database queries per request - * - * @param string (optional) Gateway ID for getting tokens for a specific gateway. - * @param int (optional) The customer id - defaults to the current user. - * @return array of WC_Payment_Token objects - * @since 2.2.7 - */ - public static function get_customer_tokens( $gateway_id = '', $customer_id = '' ) { - if ( '' === $customer_id ) { - $customer_id = get_current_user_id(); - } - - if ( ! isset( self::$customer_tokens[ $customer_id ][ $gateway_id ] ) ) { - self::$customer_tokens[ $customer_id ][ $gateway_id ] = WC_Payment_Tokens::get_customer_tokens( $customer_id, $gateway_id ); - } - - return self::$customer_tokens[ $customer_id ][ $gateway_id ]; - } - - /** - * Get the customer's alternative token. - * - * @param WC_Payment_Token $token The token to find an alternative for - * @return WC_Payment_Token The customer's alternative token - * @since 2.2.7 - */ - public static function get_customers_alternative_token( $token ) { - $payment_tokens = self::get_customer_tokens( $token->get_gateway_id(), $token->get_user_id() ); - $alternative_token = null; - - // Remove the token we're trying to find an alternative for. - unset( $payment_tokens[ $token->get_id() ] ); - - if ( count( $payment_tokens ) === 1 ) { - $alternative_token = reset( $payment_tokens ); - } else { - foreach ( $payment_tokens as $payment_token ) { - // If there is a default token we can use it as an alternative. - if ( $payment_token->is_default() ) { - $alternative_token = $payment_token; - break; - } - } - } - - return $alternative_token; - } - - /** - * Determine if the customer has an alternative token. - * - * @param WC_Payment_Token payment token object - * @return bool - * @since 2.2.7 - */ - public static function customer_has_alternative_token( $token ) { - return self::get_customers_alternative_token( $token ) !== null; - } - /** * Display a notice when a customer sets a new default token notifying them of what this means for their subscriptions. * @@ -268,12 +135,12 @@ class WCS_My_Account_Payment_Methods { */ public static function display_default_payment_token_change_notice( $default_token_id, $default_token ) { $display_notice = false; - $customer_tokens = self::get_customer_tokens( $default_token->get_gateway_id(), $default_token->get_user_id() ); + $customer_tokens = WCS_Payment_Tokens::get_customer_tokens( $default_token->get_user_id(), $default_token->get_gateway_id() ); unset( $customer_tokens[ $default_token_id ] ); // Check if there are subscriptions for one of the customer's other tokens. foreach ( $customer_tokens as $token ) { - if ( count( self::get_subscriptions_by_token( $token ) ) > 0 ) { + if ( count( WCS_Payment_Tokens::get_subscriptions_from_token( $token ) ) > 0 ) { $display_notice = true; break; } @@ -317,14 +184,12 @@ class WCS_My_Account_Payment_Methods { return; } - $tokens = self::get_customer_tokens( $default_token->get_gateway_id(), $default_token->get_user_id() ); + $tokens = WCS_Payment_Tokens::get_customer_tokens( $default_token->get_user_id(), $default_token->get_gateway_id() ); unset( $tokens[ $default_token_id ] ); foreach ( $tokens as $old_token ) { - foreach ( self::get_subscriptions_by_token( $old_token ) as $subscription ) { - $subscription = wcs_get_subscription( $subscription ); - - if ( ! empty( $subscription ) && self::update_subscription_token( $subscription, $default_token, $old_token ) ) { + foreach ( WCS_Payment_Tokens::get_subscriptions_from_token( $old_token ) as $subscription ) { + if ( ! empty( $subscription ) && WCS_Payment_Tokens::update_subscription_token( $subscription, $default_token, $old_token ) ) { $subscription->add_order_note( sprintf( _x( 'Payment method meta updated after customer changed their default token and opted to update their subscriptions. Payment meta changed from %1$s to %2$s', 'used in subscription note', 'woocommerce-subscriptions' ), $old_token->get_token(), $default_token->get_token() ) ); } } @@ -333,4 +198,49 @@ class WCS_My_Account_Payment_Methods { wp_redirect( remove_query_arg( array( 'update-subscription-tokens', 'token-id', '_wcsnonce' ) ) ); exit(); } + + /** + * Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method, + * customer id and token value stored in post meta will be returned. + * + * @since 2.2.7 + * @deprecated 2.5.0 + */ + public static function get_subscriptions_by_token( $payment_token ) { + _deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_subscriptions_from_token()' ); + return WCS_Payment_Tokens::get_subscriptions_from_token( $payment_token ); + } + + /** + * Get a list of customer payment tokens. Caches results to avoid multiple database queries per request + * + * @since 2.2.7 + * @deprecated 2.5.0 + */ + public static function get_customer_tokens( $gateway_id = '', $customer_id = '' ) { + _deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_customer_tokens()' ); + return WCS_Payment_Tokens::get_customer_tokens( $customer_id, $gateway_id ); + } + + /** + * Get the customer's alternative token. + * + * @since 2.2.7 + * @deprecated 2.5.0 + */ + public static function get_customers_alternative_token( $token ) { + _deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_customers_alternative_token()' ); + return WCS_Payment_Tokens::get_customers_alternative_token( $token ); + } + + /** + * Determine if the customer has an alternative token. + * + * @since 2.2.7 + * @deprecated 2.5.0 + */ + public static function customer_has_alternative_token( $token ) { + _deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::customer_has_alternative_token()' ); + return WCS_Payment_Tokens::customer_has_alternative_token( $token ); + } } diff --git a/includes/class-wcs-payment-tokens.php b/includes/class-wcs-payment-tokens.php new file mode 100755 index 0000000..05c8401 --- /dev/null +++ b/includes/class-wcs-payment-tokens.php @@ -0,0 +1,180 @@ +get_gateway_id(); + $payment_meta_table = self::get_subscription_payment_meta( $subscription, $token_payment_gateway ); + $token_meta_key = ''; + + // Attempt to find the token meta key from the subscription payment meta and the old token. + if ( is_array( $payment_meta_table ) ) { + foreach ( $payment_meta_table as $meta_table => $meta ) { + foreach ( $meta as $meta_key => $meta_data ) { + if ( $old_token->get_token() === $meta_data['value'] ) { + $token_meta_key = $meta_key; + break 2; + } + } + } + } + + $updated = update_post_meta( $subscription->get_id(), $token_meta_key, $new_token->get_token(), $old_token->get_token() ); + + if ( $updated ) { + do_action( 'woocommerce_subscription_token_changed', $subscription, $new_token, $old_token ); + } + + return $updated; + } + + /** + * Get all payment meta on a subscription for a gateway. + * + * @param WC_Subscription $subscription The subscription to update. + * @param string $gateway_id The target gateway ID. + * @return bool|array Payment meta data. False if no meta is found. + * @since 2.5.0 + */ + public static function get_subscription_payment_meta( $subscription, $gateway_id ) { + $payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription ); + if ( is_array( $payment_method_meta ) && isset( $payment_method_meta[ $gateway_id ] ) && is_array( $payment_method_meta[ $gateway_id ] ) ) { + return $payment_method_meta[ $gateway_id ]; + } + + return false; + } + + /** + * Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method, + * customer id and token value stored in post meta will be returned. + * + * @param WC_Payment_Token $payment_token Payment token object. + * @return array subscription posts + * @since 2.5.0 + */ + public static function get_subscriptions_from_token( $payment_token ) { + + $meta_query = array( + array( + 'key' => '_payment_method', + 'value' => $payment_token->get_gateway_id(), + ), + array( + 'key' => '_requires_manual_renewal', + 'value' => 'false', + ), + array( + 'value' => $payment_token->get_token(), + ), + ); + $subscription_ids = get_posts( array( + 'post_type' => 'shop_subscription', + 'post_status' => array( 'wc-pending', 'wc-active', 'wc-on-hold' ), + 'meta_query' => $meta_query, + 'posts_per_page' => -1, + 'fields' => 'ids', + 'post__in' => WCS_Customer_Store::instance()->get_users_subscription_ids( $payment_token->get_user_id() ), + ) ); + + if ( has_filter( 'woocommerce_subscriptions_by_payment_token' ) ) { + wcs_deprecated_function( 'The "woocommerce_subscriptions_by_payment_token" hook should no longer be used. It previously filtered post objects and in moving to CRUD and Subscription APIs the "woocommerce_subscriptions_by_payment_token"', '2.5.0', 'woocommerce_subscriptions_from_payment_token' ); + + $subscription_posts = apply_filters( 'woocommerce_subscriptions_by_payment_token', array_map( 'get_post', $subscription_ids ), $payment_token ); + $subscription_ids = array_unique( array_merge( $subscription_ids, wp_list_pluck( $subscription_posts, 'ID' ) ) ); + } + + $user_subscriptions = array(); + foreach ( $subscription_ids as $subscription_id ) { + $user_subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id ); + } + + return apply_filters( 'woocommerce_subscriptions_from_payment_token', $user_subscriptions, $payment_token ); + } + + /** + * Get a list of customer payment tokens. Caches results to avoid multiple database queries per request + * + * @param int (optional) The customer id - defaults to the current user. + * @param string (optional) Gateway ID for getting tokens for a specific gateway. + * @return array of WC_Payment_Token objects. + * @since 2.2.7 + */ + public static function get_customer_tokens( $customer_id = '', $gateway_id = '' ) { + if ( '' === $customer_id ) { + $customer_id = get_current_user_id(); + } + + if ( ! isset( self::$customer_tokens[ $customer_id ][ $gateway_id ] ) ) { + self::$customer_tokens[ $customer_id ][ $gateway_id ] = parent::get_customer_tokens( $customer_id, $gateway_id ); + } + + return self::$customer_tokens[ $customer_id ][ $gateway_id ]; + } + + /** + * Get the customer's alternative token. + * + * @param WC_Payment_Token $token The token to find an alternative for. + * @return WC_Payment_Token The customer's alternative token. + * @since 2.2.7 + */ + public static function get_customers_alternative_token( $token ) { + $payment_tokens = self::get_customer_tokens( $token->get_gateway_id(), $token->get_user_id() ); + $alternative_token = null; + + // Remove the token we're trying to find an alternative for. + unset( $payment_tokens[ $token->get_id() ] ); + + if ( count( $payment_tokens ) === 1 ) { + $alternative_token = reset( $payment_tokens ); + } else { + foreach ( $payment_tokens as $payment_token ) { + // If there is a default token we can use it as an alternative. + if ( $payment_token->is_default() ) { + $alternative_token = $payment_token; + break; + } + } + } + + return $alternative_token; + } + + /** + * Determine if the customer has an alternative token. + * + * @param WC_Payment_Token $token Payment token object. + * @return bool + * @since 2.2.7 + */ + public static function customer_has_alternative_token( $token ) { + return self::get_customers_alternative_token( $token ) !== null; + } + +} diff --git a/includes/class-wcs-permalink-manager.php b/includes/class-wcs-permalink-manager.php new file mode 100755 index 0000000..f80d07e --- /dev/null +++ b/includes/class-wcs-permalink-manager.php @@ -0,0 +1,96 @@ +set_simple_content( + // translators: 1$-2$: opening and closing tags. + sprintf( esc_html__( 'Error saving Subscriptions endpoints: %1$sSubscriptions%2$s, %1$sView subscription%2$s and %1$sSubscription payment method%2$s cannot be the same. The changes have been reverted.', 'woocommerce-subscriptions' ), '', '' ) + ); + $notice->display(); + } + } +} diff --git a/includes/class-wcs-query.php b/includes/class-wcs-query.php index 1af0db7..7f49076 100755 --- a/includes/class-wcs-query.php +++ b/includes/class-wcs-query.php @@ -17,12 +17,18 @@ class WCS_Query extends WC_Query { add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); add_action( 'parse_request', array( $this, 'parse_request' ), 0 ); add_filter( 'woocommerce_get_breadcrumb', array( $this, 'add_breadcrumb' ), 10 ); + 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' ) ); - add_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ), 10, 4 ); + + // Since WC 3.3.0, add_wcs_query_vars() is enough for custom endpoints to work. + if ( WC_Subscriptions::is_woocommerce_pre( '3.3.0' ) ) { + add_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ), 10, 4 ); + } + add_filter( 'woocommerce_get_endpoint_url', array( $this, 'maybe_redirect_to_only_subscription' ), 10, 2 ); add_action( 'woocommerce_account_subscriptions_endpoint', array( $this, 'endpoint_content' ) ); } @@ -47,6 +53,7 @@ class WCS_Query extends WC_Query { ); if ( ! WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) { $this->query_vars['subscriptions'] = get_option( 'woocommerce_myaccount_subscriptions_endpoint', 'subscriptions' ); + $this->query_vars['subscription-payment-method'] = get_option( 'woocommerce_myaccount_subscription_payment_method_endpoint', 'subscription-payment-method' ); } } @@ -123,6 +130,12 @@ class WCS_Query extends WC_Query { * @return array */ public function add_menu_items( $menu_items ) { + + // If the Subscriptions endpoint setting is empty, don't display it in line with core WC behaviour. + if ( empty( $this->query_vars['subscriptions'] ) ) { + return $menu_items; + } + if ( 1 == count( wcs_get_users_subscriptions() ) && apply_filters( 'wcs_my_account_redirect_to_single_subscription', true ) ) { $label = __( 'My Subscription', 'woocommerce-subscriptions' ); } else { @@ -148,7 +161,7 @@ class WCS_Query extends WC_Query { * @return string */ public function maybe_redirect_to_only_subscription( $url, $endpoint ) { - if ( 'subscriptions' == $endpoint && is_account_page() ) { + if ( $this->query_vars['subscriptions'] === $endpoint && is_account_page() ) { $subscriptions = wcs_get_users_subscriptions(); if ( is_array( $subscriptions ) && 1 == count( $subscriptions ) && apply_filters( 'wcs_my_account_redirect_to_single_subscription', true ) ) { @@ -216,6 +229,41 @@ class WCS_Query extends WC_Query { } } + /** + * Redirect to order-pay flow for Subscription Payment Method endpoint. + * + * @param WP_Query $query WordPress query object + * @since 2.5.0 + */ + public function maybe_redirect_payment_methods( $query ) { + + if ( ! $query->is_main_query() || ! absint( $query->get( 'subscription-payment-method' ) ) ) { + return; + } + + $subscription = wcs_get_subscription( absint( $query->get( 'subscription-payment-method' ) ) ); + if ( ! $subscription ) { + return; + } + + if ( ! $subscription->can_be_updated_to( 'new-payment-method' ) ) { + + $url = $subscription->get_view_order_url(); + wc_add_notice( __( 'The payment method can not be changed for that subscription.', 'woocommerce-subscriptions' ), 'error' ); + + } else { + + $args = array( + 'change_payment_method' => $subscription->get_id(), + '_wpnonce' => wp_create_nonce(), + ); + $url = add_query_arg( $args, $subscription->get_checkout_payment_url() ); + } + + wp_redirect( $url ); + exit(); + } + /** * Reset the woocommerce_myaccount_view_subscriptions_endpoint option name to woocommerce_myaccount_view_subscription_endpoint * @@ -262,8 +310,16 @@ class WCS_Query extends WC_Query { 'desc_tip' => true, ); - WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_myaccount_view_order_endpoint', array( $subscriptions_endpoint_setting, $view_subscription_endpoint_setting ), 'multiple_settings' ); + $subscription_payment_method_endpoint_setting = array( + 'title' => __( 'Subscription payment method', 'woocommerce-subscriptions' ), + 'desc' => __( 'Endpoint for the My Account → Change Subscription Payment Method page', 'woocommerce-subscriptions' ), + 'id' => 'woocommerce_myaccount_subscription_payment_method_endpoint', + 'type' => 'text', + 'default' => 'subscription-payment-method', + 'desc_tip' => true, + ); + WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_myaccount_view_order_endpoint', array( $subscriptions_endpoint_setting, $view_subscription_endpoint_setting, $subscription_payment_method_endpoint_setting ), 'multiple_settings' ); return $settings; } @@ -279,7 +335,7 @@ class WCS_Query extends WC_Query { * @return string $url */ - public function get_endpoint_url( $url, $endpoint, $value = '', $permalink = '') { + public function get_endpoint_url( $url, $endpoint, $value = '', $permalink = '' ) { if ( ! empty( $this->query_vars[ $endpoint ] ) ) { remove_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ) ); diff --git a/includes/class-wcs-retry-manager.php b/includes/class-wcs-retry-manager.php index 4036ded..95d4bfc 100755 --- a/includes/class-wcs-retry-manager.php +++ b/includes/class-wcs-retry-manager.php @@ -63,7 +63,8 @@ class WCS_Retry_Manager { add_action( 'woocommerce_subscriptions_retry_status_updated', __CLASS__ . '::maybe_delete_payment_retry_date', 0, 2 ); - add_action( 'woocommerce_subscription_renewal_payment_failed', __CLASS__ . '::maybe_apply_retry_rule', 10, 2 ); + add_action( 'woocommerce_subscription_renewal_payment_failed', array( __CLASS__, 'maybe_apply_retry_rule' ), 10, 2 ); + add_action( 'woocommerce_subscription_renewal_payment_failed', array( __CLASS__, 'maybe_reapply_last_retry_rule' ), 15, 2 ); add_action( 'woocommerce_scheduled_subscription_payment_retry', __CLASS__ . '::maybe_retry_payment' ); @@ -196,13 +197,13 @@ class WCS_Retry_Manager { /** * When a payment fails, apply a retry rule, if one exists that applies to this failure. * - * @param WC_Subscription The subscription on which the payment failed - * @param WC_Order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param) + * @param WC_Subscription $subscription The subscription on which the payment failed. + * @param WC_Order $last_order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param). + * * @since 2.1 */ public static function maybe_apply_retry_rule( $subscription, $last_order ) { - - if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) ) { + if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) || ! self::is_scheduled_payment_attempt() ) { return; } @@ -231,7 +232,7 @@ class WCS_Retry_Manager { } if ( $retry_rule->get_retry_interval() > 0 ) { - // by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action + // by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action. $subscription->update_dates( array( 'payment_retry' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval( $retry_count ) ) ) ); } @@ -239,6 +240,36 @@ class WCS_Retry_Manager { } } + /** + * (Maybe) reapply last retry rule if: + * - Payment is no-scheduled + * - $last_order contains a Retry + * - Retry contains a rule + * + * @param WC_Subscription $subscription The subscription on which the payment failed. + * @param WC_Order $last_order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param). + * + * @since 2.5.0 + */ + public static function maybe_reapply_last_retry_rule( $subscription, $last_order ) { + // We're only interested in non-automatic payment attempts. + if ( self::is_scheduled_payment_attempt() ) { + return; + } + + $last_retry = self::store()->get_last_retry_for_order( $last_order->get_id() ); + if ( ! $last_retry || 'pending' !== $last_retry->get_status() || null === ( $last_retry_rule = $last_retry->get_rule() ) ) { + return; + } + + foreach ( array( 'order' => $last_order, 'subscription' => $subscription ) as $object_type => $object ) { + $new_status = $last_retry_rule->get_status_to_apply( $object_type ); + if ( '' !== $new_status && ! $object->has_status( $new_status ) ) { + $object->update_status( $new_status, _x( 'Retry rule reapplied:', 'used in order note as reason for why status changed', 'woocommerce-subscriptions' ) ); + } + } + } + /** * When a retry hook is triggered, check if the rules for that retry are still valid * and if so, retry the payment. @@ -391,9 +422,27 @@ class WCS_Retry_Manager { } } + /** + * Is `woocommerce_scheduled_subscription_payment` or `woocommerce_scheduled_subscription_payment_retry` current action? + * + * @return boolean + * + * @since 2.5.0 + */ + protected static function is_scheduled_payment_attempt() { + /** + * Filter 'Is scheduled payment attempt?' + * + * @param boolean doing_action( 'woocommerce_scheduled_subscription_payment' ) || doing_action( 'woocommerce_scheduled_subscription_payment_retry' ) + * @since 2.5.0 + */ + return (bool) apply_filters( 'wcs_is_scheduled_payment_attempt', doing_action( 'woocommerce_scheduled_subscription_payment' ) || doing_action( 'woocommerce_scheduled_subscription_payment_retry' ) ); + } + /** * Access the object used to interface with the store. * + * @return WCS_Retry_Store * @since 2.4 */ public static function store() { diff --git a/includes/class-wcs-template-loader.php b/includes/class-wcs-template-loader.php index b23ae35..4a2000d 100755 --- a/includes/class-wcs-template-loader.php +++ b/includes/class-wcs-template-loader.php @@ -8,39 +8,27 @@ class WCS_Template_Loader { public static function init() { - add_filter( 'wc_get_template', array( __CLASS__, 'add_view_subscription_template' ), 10, 5 ); add_action( 'woocommerce_account_view-subscription_endpoint', array( __CLASS__, 'get_view_subscription_template' ) ); add_action( 'woocommerce_subscription_details_table', array( __CLASS__, 'get_subscription_details_template' ) ); add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_subscription_totals_template' ) ); + add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_order_downloads_template' ), 20 ); } /** - * Show the subscription template when view a subscription instead of loading the default order template. - * - * @param $located - * @param $template_name - * @param $args - * @param $template_path - * @param $default_path - * @since 2.0 - */ - public static function add_view_subscription_template( $located, $template_name, $args, $template_path, $default_path ) { - global $wp; - - if ( 'myaccount/my-account.php' == $template_name && ! empty( $wp->query_vars['view-subscription'] ) && WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) { - $located = wc_locate_template( 'myaccount/view-subscription.php', $template_path, plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); - } - - return $located; - } - - /** - * Get the view subscription template. A post WC v2.6 compatible version of @see WCS_Template_Loader::add_view_subscription_template() + * Get the view subscription template. * + * @param int $subscription_id Subscription ID. * @since 2.0.17 */ - public static function get_view_subscription_template() { - wc_get_template( 'myaccount/view-subscription.php', array(), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); + public static function get_view_subscription_template( $subscription_id ) { + $subscription = wcs_get_subscription( absint( $subscription_id ) ); + + if ( ! $subscription || ! current_user_can( 'view_order', $subscription->get_id() ) ) { + echo '
    ' . esc_html__( 'Invalid Subscription.', 'woocommerce-subscriptions' ) . ' '. esc_html__( 'My Account', 'woocommerce-subscriptions' ) .'' . '
    '; + return; + } + + wc_get_template( 'myaccount/view-subscription.php', compact( 'subscription' ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); } /** @@ -62,4 +50,16 @@ class WCS_Template_Loader { public static function get_subscription_totals_template( $subscription ) { wc_get_template( 'myaccount/subscription-totals.php', array( 'subscription' => $subscription ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); } + + /** + * Get the order downloads template, which is part of the view subscription page. + * + * @param WC_Subscription $subscription Subscription object + * @since 2.5.0 + */ + public static function get_order_downloads_template( $subscription ) { + if ( $subscription->has_downloadable_item() && $subscription->is_download_permitted() ) { + wc_get_template( 'order/order-downloads.php', array( 'downloads' => $subscription->get_downloadable_items(), 'show_title' => true ) ); + } + } } diff --git a/includes/data-stores/class-wcs-subscription-data-store-cpt.php b/includes/data-stores/class-wcs-subscription-data-store-cpt.php index 21c8a27..d1f2a2f 100755 --- a/includes/data-stores/class-wcs-subscription-data-store-cpt.php +++ b/includes/data-stores/class-wcs-subscription-data-store-cpt.php @@ -155,20 +155,18 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements } } - // On WC 3.5.0 the ID of the user that placed the order was moved from the post meta _customer_user to the post_author field in the wp_posts table. - // If the update routine didn't manage to cover subscriptions, we need to use the value stored as post meta until our own update finishes. - if ( version_compare( get_option( 'woocommerce_db_version' ), '3.5.0', '>=' ) && 1 == $post_object->post_author && get_option( 'wcs_subscription_post_author_upgrade_is_scheduled', false ) ) { - $props_to_set['customer_id'] = get_post_meta( $subscription->get_id(), '_customer_user', true ); - } else { - /** - * WC 3.5.0 and our 2.4.0 post author upgrade scripts didn't account for subscriptions created manually by admin users with a user ID not equal to 1. - * This resulted in those subscription post author columns not being updated and so linked to the admin user who created them, not the customer. - * - * Until a permanent fix is found, revert to the previous behavior of getting the customer user from post meta. - * This will be eventually removed. - * - * @see https://github.com/Prospress/woocommerce-subscriptions/issues/3036 - */ + /** + * WC 3.5.0 and our 2.4.0 post author upgrade scripts didn't account for subscriptions created manually by admin users with a user ID not equal to 1. + * This resulted in those subscription post author columns not being updated and so linked to the admin user who created them, not the customer. + * + * To make sure all subscriptions are linked to the correct customer, we revert to the previous behavior of + * getting the customer user from post meta. + * The fix is only applied on WC 3.5.0 because 3.5.1 brought back the old way (pre 3.5.0) of getting the + * customer ID for orders. + * + * @see https://github.com/Prospress/woocommerce-subscriptions/issues/3036 + */ + if ( '3.5.0' === WC()->version ) { $props_to_set['customer_id'] = get_post_meta( $subscription->get_id(), '_customer_user', true ); } diff --git a/includes/early-renewal/class-wcs-cart-early-renewal.php b/includes/early-renewal/class-wcs-cart-early-renewal.php index a8889d5..8ca8607 100755 --- a/includes/early-renewal/class-wcs-cart-early-renewal.php +++ b/includes/early-renewal/class-wcs-cart-early-renewal.php @@ -25,6 +25,7 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { // Check if a user is requesting to create an early renewal order for a subscription. add_action( 'template_redirect', array( $this, 'maybe_setup_cart' ), 100 ); + add_action( 'woocommerce_checkout_create_order', array( $this, 'copy_subscription_meta_to_order' ), 90 ); // Record early renewal payments. if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { add_action( 'woocommerce_checkout_order_processed', array( $this, 'maybe_record_early_renewal' ), 100, 2 ); @@ -149,6 +150,26 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { $subscription->update_status( 'on-hold', _x( 'Customer requested to renew early:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) ); } + /** + * Copies the metadata from the subscription to the order created on checkout. + * + * @param WC_Order $order The WC Order object. + * + * @since 2.5.2 + */ + public function copy_subscription_meta_to_order( $order ) { + $cart_item = $this->cart_contains(); + if ( ! $cart_item ) { + return; + } + + // Get the subscription. + $subscription = wcs_get_subscription( $cart_item[ $this->cart_item_key ]['subscription_id'] ); + + // Copy all meta from subscription to new renewal order + wcs_copy_order_meta( $subscription, $order, 'renewal_order' ); + } + /** * Adds the early renewal metadata to the order created on checkout. * diff --git a/includes/emails/class-wcs-email-cancelled-subscription.php b/includes/emails/class-wcs-email-cancelled-subscription.php index 0a7b5c8..38eb1b2 100755 --- a/includes/emails/class-wcs-email-cancelled-subscription.php +++ b/includes/emails/class-wcs-email-cancelled-subscription.php @@ -45,6 +45,26 @@ class WCS_Email_Cancelled_Subscription extends WC_Email { } } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * trigger function. * @@ -131,22 +151,22 @@ class WCS_Email_Cancelled_Subscription extends WC_Email { 'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ), 'type' => 'text', // translators: placeholder is admin email - 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ), + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), 'placeholder' => '', 'default' => '', ), 'subject' => array( 'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ), 'type' => 'text', - 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), $this->subject ), - 'placeholder' => '', + 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '' . $this->subject . '' ), + 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'heading' => array( 'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ), 'type' => 'text', - 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), $this->heading ), - 'placeholder' => '', + 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), '' . $this->heading . '' ), + 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'email_type' => array( diff --git a/includes/emails/class-wcs-email-completed-renewal-order.php b/includes/emails/class-wcs-email-completed-renewal-order.php index 9821d43..037aa85 100755 --- a/includes/emails/class-wcs-email-completed-renewal-order.php +++ b/includes/emails/class-wcs-email-completed-renewal-order.php @@ -46,6 +46,26 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde WC_Email::__construct(); } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * trigger function. * diff --git a/includes/emails/class-wcs-email-completed-switch-order.php b/includes/emails/class-wcs-email-completed-switch-order.php index d04d30c..b46cc32 100755 --- a/includes/emails/class-wcs-email-completed-switch-order.php +++ b/includes/emails/class-wcs-email-completed-switch-order.php @@ -45,6 +45,26 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order WC_Email::__construct(); } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * trigger function. * diff --git a/includes/emails/class-wcs-email-customer-payment-retry.php b/includes/emails/class-wcs-email-customer-payment-retry.php index bd416cb..a03e576 100755 --- a/includes/emails/class-wcs-email-customer-payment-retry.php +++ b/includes/emails/class-wcs-email-customer-payment-retry.php @@ -36,6 +36,28 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic WC_Email::__construct(); } + /** + * Get the default e-mail subject. + * + * @param bool $paid Whether the order has been paid or not. + * @since 2.5.3 + * @return string + */ + public function get_default_subject( $paid = false ) { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @param bool $paid Whether the order has been paid or not. + * @since 2.5.3 + * @return string + */ + public function get_default_heading( $paid = false ) { + return $this->heading; + } + /** * trigger function. * @@ -53,9 +75,9 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic $retry_time_index = array_search( '{retry_time}', $this->find ); if ( false === $retry_time_index ) { $this->find['retry_time'] = '{retry_time}'; - $this->replace['retry_time'] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) ); + $this->replace['retry_time'] = wcs_get_human_time_diff( $this->retry->get_time() ); } else { - $this->replace[ $retry_time_index ] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) ); + $this->replace[ $retry_time_index ] = wcs_get_human_time_diff( $this->retry->get_time() ); } parent::trigger( $order_id, $order ); diff --git a/includes/emails/class-wcs-email-customer-renewal-invoice.php b/includes/emails/class-wcs-email-customer-renewal-invoice.php index 82caa1c..4259a00 100755 --- a/includes/emails/class-wcs-email-customer-renewal-invoice.php +++ b/includes/emails/class-wcs-email-customer-renewal-invoice.php @@ -56,6 +56,28 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice { WC_Email::__construct(); } + /** + * Get the default e-mail subject. + * + * @param bool $paid Whether the order has been paid or not. + * @since 2.5.3 + * @return string + */ + public function get_default_subject( $paid = false ) { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @param bool $paid Whether the order has been paid or not. + * @since 2.5.3 + * @return string + */ + public function get_default_heading( $paid = false ) { + return $this->heading; + } + /** * trigger function. * diff --git a/includes/emails/class-wcs-email-expired-subscription.php b/includes/emails/class-wcs-email-expired-subscription.php index d1c80d3..01111b8 100755 --- a/includes/emails/class-wcs-email-expired-subscription.php +++ b/includes/emails/class-wcs-email-expired-subscription.php @@ -45,6 +45,26 @@ class WCS_Email_Expired_Subscription extends WC_Email { } } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * trigger function. * @@ -129,22 +149,22 @@ class WCS_Email_Expired_Subscription extends WC_Email { 'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ), 'type' => 'text', // translators: placeholder is admin email - 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ), + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '' . esc_html( get_option( 'admin_email' ) ) . '' ), 'placeholder' => '', 'default' => '', ), 'subject' => array( 'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ), 'type' => 'text', - 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), $this->subject ), - 'placeholder' => '', + 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '' . $this->subject . '' ), + 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'heading' => array( 'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ), 'type' => 'text', 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), $this->heading ), - 'placeholder' => '', + 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'email_type' => array( diff --git a/includes/emails/class-wcs-email-new-renewal-order.php b/includes/emails/class-wcs-email-new-renewal-order.php index ecd3869..ffcedc3 100755 --- a/includes/emails/class-wcs-email-new-renewal-order.php +++ b/includes/emails/class-wcs-email-new-renewal-order.php @@ -48,6 +48,26 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order { } } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * trigger function. * diff --git a/includes/emails/class-wcs-email-new-switch-order.php b/includes/emails/class-wcs-email-new-switch-order.php index 6e8b31d..83ab856 100755 --- a/includes/emails/class-wcs-email-new-switch-order.php +++ b/includes/emails/class-wcs-email-new-switch-order.php @@ -43,6 +43,26 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order { } } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * trigger function. * diff --git a/includes/emails/class-wcs-email-on-hold-subscription.php b/includes/emails/class-wcs-email-on-hold-subscription.php index d42f9e1..e17b90c 100755 --- a/includes/emails/class-wcs-email-on-hold-subscription.php +++ b/includes/emails/class-wcs-email-on-hold-subscription.php @@ -45,6 +45,26 @@ class WCS_Email_On_Hold_Subscription extends WC_Email { } } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * trigger function. * @@ -129,22 +149,22 @@ class WCS_Email_On_Hold_Subscription extends WC_Email { 'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ), 'type' => 'text', // translators: placeholder is admin email - 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ), + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), 'placeholder' => '', 'default' => '', ), 'subject' => array( 'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ), 'type' => 'text', - 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), $this->subject ), - 'placeholder' => '', + 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '' . $this->subject . '' ), + 'placeholder' => $this->get_default_subject(), 'default' => '', ), 'heading' => array( 'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ), 'type' => 'text', 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), $this->heading ), - 'placeholder' => '', + 'placeholder' => $this->get_default_heading(), 'default' => '', ), 'email_type' => array( diff --git a/includes/emails/class-wcs-email-payment-retry.php b/includes/emails/class-wcs-email-payment-retry.php index d1cd514..a4d4f27 100755 --- a/includes/emails/class-wcs-email-payment-retry.php +++ b/includes/emails/class-wcs-email-payment-retry.php @@ -40,6 +40,26 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order { $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * Trigger. * @@ -53,7 +73,7 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order { $this->find['retry-time'] = '{retry_time}'; $this->replace['order-date'] = wcs_format_datetime( wcs_get_objects_property( $this->object, 'date_created' ) ); $this->replace['order-number'] = $this->object->get_order_number(); - $this->replace['retry-time'] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) ); + $this->replace['retry-time'] = wcs_get_human_time_diff( $this->retry->get_time() ); if ( ! $this->is_enabled() || ! $this->get_recipient() ) { return; diff --git a/includes/emails/class-wcs-email-processing-renewal-order.php b/includes/emails/class-wcs-email-processing-renewal-order.php index 70c6899..484a201 100755 --- a/includes/emails/class-wcs-email-processing-renewal-order.php +++ b/includes/emails/class-wcs-email-processing-renewal-order.php @@ -40,6 +40,26 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or WC_Email::__construct(); } + /** + * Get the default e-mail subject. + * + * @since 2.5.3 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get the default e-mail heading. + * + * @since 2.5.3 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + /** * trigger function. * diff --git a/includes/gateways/class-wc-subscriptions-payment-gateways.php b/includes/gateways/class-wc-subscriptions-payment-gateways.php index d364984..88ea101 100755 --- a/includes/gateways/class-wc-subscriptions-payment-gateways.php +++ b/includes/gateways/class-wc-subscriptions-payment-gateways.php @@ -21,16 +21,18 @@ class WC_Subscriptions_Payment_Gateways { */ public static function init() { - add_action( 'init', __CLASS__ . '::init_paypal', 5 ); // run before default priority 10 in case the site is using ALTERNATE_WP_CRON to avoid https://core.trac.wordpress.org/ticket/24160 + add_action( 'init', __CLASS__ . '::init_paypal', 5 ); // run before default priority 10 in case the site is using ALTERNATE_WP_CRON to avoid https://core.trac.wordpress.org/ticket/24160. add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways' ); add_filter( 'woocommerce_no_available_payment_methods_message', __CLASS__ . '::no_available_payment_methods_message' ); - // Trigger a hook for gateways to charge recurring payments + add_filter( 'woocommerce_payment_gateways_renewal_support_status_html', __CLASS__ . '::payment_gateways_support_tooltip', 11, 2 ); + + // Trigger a hook for gateways to charge recurring payments. add_action( 'woocommerce_scheduled_subscription_payment', __CLASS__ . '::gateway_scheduled_subscription_payment', 10, 1 ); - // Create a gateway specific hooks for subscription events + // Create a gateway specific hooks for subscription events. add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::trigger_gateway_status_updated_hook', 10, 2 ); } @@ -214,6 +216,73 @@ class WC_Subscriptions_Payment_Gateways { } } + /** + * Display a list of each gateway supported features in a tooltip + * + * @since 2.5.0 + */ + public static function payment_gateways_support_tooltip( $status_html, $gateway ) { + + if ( ( ! is_array( $gateway->supports ) || ! in_array( 'subscriptions', $gateway->supports ) ) && 'paypal' !== $gateway->id ) { + return $status_html; + } + + $core_features = $gateway->supports; + $subscription_features = $change_payment_method_features = array(); + + foreach ( $core_features as $key => $feature ) { + + // Skip any non-subscription related features. + if ( 0 !== strpos( $feature, 'subscription' ) ) { + continue; + } + + $feature = str_replace( 'subscription_', '', $feature ); + + if ( 0 === strpos( $feature, 'payment_method' ) ) { + switch ( $feature ) { + case 'payment_method_change': + $change_payment_method_features[] = 'payment method change'; + break; + + case 'payment_method_change_customer': + $change_payment_method_features[] = 'customer change payment'; + break; + + case 'payment_method_change_admin': + $change_payment_method_features[] = 'admin change payment'; + break; + + default: + $change_payment_method_features[] = str_replace( 'payment_method', ' ', $feature ); + break; + } + } else { + $subscription_features[] = $feature; + } + + unset( $core_features[ $key ] ); + } + + $status_html .= ''; + + $allowed_html = wp_kses_allowed_html( 'post' ); + $allowed_html['span']['data-tip'] = true; + + return wp_kses( $status_html, $allowed_html ); + } + /** * Fire a gateway specific hook for when a subscription is activated. * diff --git a/includes/gateways/paypal/class-wcs-paypal.php b/includes/gateways/paypal/class-wcs-paypal.php index 250b1da..0c42dbf 100755 --- a/includes/gateways/paypal/class-wcs-paypal.php +++ b/includes/gateways/paypal/class-wcs-paypal.php @@ -34,7 +34,7 @@ class WCS_PayPal { * Main PayPal Instance, ensures only one instance is/can be loaded * * @see wc_paypal_express() - * @return WC_PayPal_Express + * @return WCS_PayPal * @since 2.0 */ public static function instance() { @@ -80,18 +80,32 @@ class WCS_PayPal { add_filter( 'woocommerce_subscriptions_admin_meta_boxes_script_parameters', __CLASS__ . '::maybe_add_change_payment_method_warning' ); + // Maybe order don't need payment because lock. + add_filter( 'woocommerce_order_needs_payment', __CLASS__ . '::maybe_override_needs_payment', 10, 2 ); + + // Remove payment lock when order is completely paid or order is cancelled. + add_action( 'woocommerce_order_status_cancelled', __CLASS__ . '::maybe_remove_payment_lock' ); + add_action( 'woocommerce_payment_complete', __CLASS__ . '::maybe_remove_payment_lock' ); + + // Adds payment lock on order received. + add_action( 'get_header', __CLASS__ . '::maybe_add_payment_lock' ); + // Run the IPN failure handler attach and detach functions before and after processing to catch and log any unexpected shutdowns add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::attach', -1, 1 ); add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::detach', 1, 1 ); + // Remove PayPal from the available payment methods if it's disabled for subscription purchases. + add_filter( 'woocommerce_available_payment_gateways', array( __CLASS__, 'maybe_remove_paypal_standard' ) ); + WCS_PayPal_Supports::init(); WCS_PayPal_Status_Manager::init(); WCS_PayPal_Standard_Switcher::init(); if ( is_admin() ) { WCS_PayPal_Admin::init(); - WCS_PayPal_Change_Payment_Method_Admin::init(); } + + WCS_PayPal_Change_Payment_Method_Admin::init(); } /** @@ -227,6 +241,11 @@ class WCS_PayPal { // Store the billing agreement ID on the order and subscriptions wcs_set_paypal_id( $order, $billing_agreement_response->get_billing_agreement_id() ); + // Update payment method on all active subscriptions? + if ( wcs_is_subscription( $order ) && WC_Subscriptions_Change_Payment_Gateway::will_subscription_update_all_payment_methods( $order ) ) { + WC_Subscriptions_Change_Payment_Gateway::update_all_payment_methods_from_subscription( $order, $payment_method->id ); + } + foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'any' ) ) as $subscription ) { $subscription->set_payment_method( $payment_method ); wcs_set_paypal_id( $subscription, $billing_agreement_response->get_billing_agreement_id() ); // Also saves the subscription @@ -438,6 +457,74 @@ class WCS_PayPal { return $script_parameters; } + /** + * This validates against payment lock for PP and returns false if we meet the criteria: + * - is a parent order. + * - payment method is paypal. + * - PayPal Reference Transactions is disabled. + * - order has lock. + * - lock hasn't timeout. + * + * @param bool $needs_payment Does this order needs to process payment? + * @param WC_Order $order The actual order. + * + * @return bool + * @since 2.5.3 + */ + public static function maybe_override_needs_payment( $needs_payment, $order ) { + if ( $needs_payment && self::instance()->get_id() === $order->get_payment_method() && ! self::are_reference_transactions_enabled() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) { + $has_lock = $order->get_meta( '_wcs_lock_order_payment' ); + $seconds_since_order = wcs_seconds_since_order_created( $order ); + + // We have lock and order hasn't meet the lock time. + if ( $has_lock && $seconds_since_order < apply_filters( 'wcs_lock_order_payment_seconds', 180 ) ) { + $needs_payment = false; + } + } + + return $needs_payment; + } + + /** + * Adds payment lock meta when order is received and... + * - order is valid. + * - payment method is paypal. + * - order needs payment. + * - PayPal Reference Transactions is disabled. + * - order is parent order of a subscription. + * + * @since 2.5.3 + */ + public static function maybe_add_payment_lock() { + if ( ! wcs_is_order_received_page() ) { + return; + } + + global $wp; + $order = wc_get_order( absint( $wp->query_vars['order-received'] ) ); + + if ( $order && self::instance()->get_id() === $order->get_payment_method() && $order->needs_payment() && ! self::are_reference_transactions_enabled() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) { + $order->update_meta_data( '_wcs_lock_order_payment', 'true' ); + $order->save(); + } + } + + /** + * Removes payment lock when order is parent and has paypal method. + * + * @param int $order_id Order cancelled/paid. + * + * @since 2.5.3 + */ + public static function maybe_remove_payment_lock( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( self::instance()->get_id() === $order->get_payment_method() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) { + $order->delete_meta_data( 'wcs_lock_order_payment' ); + $order->save(); + } + } + /** Getters ******************************************************/ /** @@ -540,4 +627,65 @@ class WCS_PayPal { return 'paypal'; } + /** + * Set the default value for whether PayPal Standard is enabled or disabled for subscriptions purchases. + * + * PayPal Standard will be enabled for subscriptions when: + * - PayPal is enabled. + * - The store has existing subscriptions. + * + * In any other case, it will be disabled by default. + * This function is called when 2.5.0 is active for the first time. @see WC_Subscriptions_Upgrader::upgrade() + * + * @since 2.5.0 + */ + public static function set_enabled_for_subscriptions_default() { + + // Exit early if it has already been set. + if ( self::get_option( 'enabled_for_subscriptions' ) ) { + return; + } + + // For existing stores with PayPal enabled, PayPal is automatically enabled for subscriptions. + if ( 'yes' === WCS_PayPal::get_option( 'enabled' ) && wcs_do_subscriptions_exist() ) { + $default = 'yes'; + } else { + $default = 'no'; + } + + // Find the PayPal Standard gateway instance to set the setting. + foreach ( WC()->payment_gateways->payment_gateways as $gateway ) { + if ( $gateway->id === 'paypal' ) { + wcs_update_settings_option( $gateway, 'enabled_for_subscriptions', $default ); + break; + } + } + } + + /** + * Remove PayPal Standard as an available payment method if it is disabled for subscriptions. + * + * @param array $available_gateways A list of available payment methods displayed on the checkout. + * @return array + * @since 2.5.0 + */ + public static function maybe_remove_paypal_standard( $available_gateways ) { + + if ( ! isset( $available_gateways['paypal'] ) || 'yes' === self::get_option( 'enabled_for_subscriptions' ) || WCS_PayPal::are_reference_transactions_enabled() ) { + return $available_gateways; + } + + $paying_for_order = absint( get_query_var( 'order-pay' ) ); + + if ( + WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment || + WC_Subscriptions_Cart::cart_contains_subscription() || + wcs_cart_contains_renewal() || + ( $paying_for_order && wcs_order_contains_subscription( $paying_for_order ) ) + ) { + unset( $available_gateways['paypal'] ); + } + + return $available_gateways; + } } diff --git a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php index b6f4b5c..1376f30 100755 --- a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php +++ b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php @@ -39,6 +39,9 @@ class WCS_PayPal_Admin { // Before WC updates the PayPal settings remove credentials error flag add_action( 'load-woocommerce_page_wc-settings', __CLASS__ . '::maybe_update_credentials_error_flag', 9 ); + + // Add an enable for subscription purchases setting. + add_action( 'woocommerce_settings_api_form_fields_paypal', array( __CLASS__, 'add_enable_for_subscriptions_setting' ) ); } /** @@ -56,7 +59,8 @@ class WCS_PayPal_Admin { // Warn store managers not to change their PayPal Email address as it can break existing Subscriptions in WC2.0+ WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['desc_tip'] = false; - WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['description'] .= '

    ' . __( 'It is strongly recommended you do not change the Receiver Email address if you have active subscriptions with PayPal. Doing so can break existing subscriptions.', 'woocommerce-subscriptions' ); + // translators: $1 and $2 are opening and closing strong tags, respectively. + WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['description'] .= '

    ' . sprintf( __( 'It is %sstrongly recommended you do not change the Receiver Email address%s if you have active subscriptions with PayPal. Doing so can break existing subscriptions.', 'woocommerce-subscriptions' ), '', '' ); } } @@ -244,7 +248,7 @@ class WCS_PayPal_Admin { * @param WC_Subscription $subscription */ public static function profile_link( $subscription ) { - if ( wcs_is_subscription( $subscription ) && 'paypal' == $subscription->get_payment_method() ) { + if ( wcs_is_subscription( $subscription ) && ! $subscription->is_manual() && 'paypal' == $subscription->get_payment_method() ) { $paypal_profile_id = wcs_get_paypal_id( $subscription ); @@ -275,4 +279,35 @@ class WCS_PayPal_Admin { } + /** + * Add the enabled or subscriptions setting. + * + * @param array $settings The WooCommerce PayPal Settings array. + * @return array + * @since 2.5.0 + */ + public static function add_enable_for_subscriptions_setting( $settings ) { + if ( WCS_PayPal::are_reference_transactions_enabled() ) { + return $settings; + } + + $setting = array( + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal Standard for Subscriptions', 'woocommerce-subscriptions' ), + 'default' => 'no', + ); + + // Display a description + if ( 'no' === WCS_PayPal::get_option( 'enabled_for_subscriptions' ) ) { + $setting['description'] = sprintf( + /* translators: Placeholders are the opening and closing link tags.*/ + __( "Before enabling PayPal Standard for Subscriptions, please note, when using PayPal Standard, customers are locked into using PayPal Standard for the life of their subscription, and PayPal Standard has a number of limitations. Please read the guide on %swhy we don't recommend PayPal Standard%s for Subscriptions before choosing to enable this option.", 'woocommerce-subscriptions' ), + '', '' + ); + } + + $settings = wcs_array_insert_after( 'enabled', $settings, 'enabled_for_subscriptions', $setting ); + + return $settings; + } } diff --git a/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php b/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php index bdc150c..f9348b6 100755 --- a/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php +++ b/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php @@ -28,7 +28,9 @@ class WCS_PayPal_Change_Payment_Method_Admin { add_filter( 'woocommerce_subscription_payment_meta', __CLASS__ . '::add_payment_meta_details', 10, 2 ); // Validate the PayPal billing agreement ID meta value when attempting to set PayPal as the payment method - add_filter( 'woocommerce_subscription_validate_payment_meta_paypal', __CLASS__ . '::validate_payment_meta', 10, 2 ); + if ( is_admin() ) { + add_filter( 'woocommerce_subscription_validate_payment_meta_paypal', __CLASS__ . '::validate_payment_meta', 10, 2 ); + } } /** diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php index 757a276..0815489 100755 --- a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php @@ -252,7 +252,7 @@ class WCS_PayPal_Reference_Transaction_API_Request { 'ITEMURL' => $product->get_permalink(), ); - $order_subtotal += $item['line_total']; + $order_subtotal += $order->get_line_total( $item ); } // add fees @@ -264,7 +264,7 @@ class WCS_PayPal_Reference_Transaction_API_Request { 'QTY' => 1, ); - $order_subtotal += $fee['line_total']; + $order_subtotal += $order->get_line_total( $fee ); } // add discounts @@ -550,18 +550,17 @@ class WCS_PayPal_Reference_Transaction_API_Request { /** * Returns the request parameters after validation & filtering * - * @throws \SV_WC_Payment_Gateway_Exception invalid amount + * @throws \Exception invalid amount * @return array request parameters * @since 2.0 */ public function get_parameters() { - /** * Filter PPE request parameters. * * Use this to modify the PayPal request parameters prior to validation * - * @param array $parameters + * @param array $parameters * @param \WC_PayPal_Express_API_Request $this instance */ $this->parameters = apply_filters( 'wcs_paypal_request_params', $this->parameters, $this ); @@ -579,8 +578,7 @@ class WCS_PayPal_Reference_Transaction_API_Request { // amounts must be 10,000.00 or less for USD if ( isset( $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] ) && 'USD' == $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] && $value > 10000 ) { - - throw new SV_WC_Payment_Gateway_Exception( sprintf( '%s amount of %s must be less than $10,000.00', $key, $value ) ); + throw new Exception( sprintf( '%s amount of %s must be less than $10,000.00', $key, wc_price( $value ) ) ); } // PayPal requires locale-specific number formats (e.g. USD is 123.45) diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php index 496e3ff..58b1450 100755 --- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php @@ -248,15 +248,16 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { switch ( $transaction_details['txn_type'] ) { case 'subscr_signup': + $order = self::get_parent_order_with_fallback( $subscription ); // Store PayPal Details on Subscription and Order $this->save_paypal_meta_data( $subscription, $transaction_details ); - $this->save_paypal_meta_data( $subscription->get_parent(), $transaction_details ); + $this->save_paypal_meta_data( $order, $transaction_details ); // When there is a free trial & no initial payment amount, we need to mark the order as paid and activate the subscription - if ( ! $is_payment_change && ! $is_renewal_sign_up_after_failure && 0 == $subscription->get_parent()->get_total() ) { + if ( ! $is_payment_change && ! $is_renewal_sign_up_after_failure && 0 == $order->get_total() ) { // Safe to assume the subscription has an order here because otherwise we wouldn't get a 'subscr_signup' IPN - $subscription->get_parent()->payment_complete(); // No 'txn_id' value for 'subscr_signup' IPN messages + $order->payment_complete(); // No 'txn_id' value for 'subscr_signup' IPN messages update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' ); } @@ -288,8 +289,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { break; case 'subscr_payment': - - if ( 0.01 == $transaction_details['mc_gross'] && 1 == $subscription->get_completed_payment_count() ) { + if ( 0.01 == $transaction_details['mc_gross'] ) { WC_Gateway_Paypal::log( 'IPN ignored, treating IPN as secondary trial period.' ); exit; } @@ -335,17 +335,18 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { // First payment on order, process payment & activate subscription if ( $is_first_payment ) { - $parent_order = $subscription->get_parent(); + $parent_order = self::get_parent_order_with_fallback( $subscription ); if ( ! $parent_order->is_paid() ) { $parent_order->payment_complete( $transaction_details['txn_id'] ); - } elseif ( $subscription->can_be_updated_to( 'active' ) ) { - // If the order has already been paid it might have been completed via PDT so reactivate the subscription now because calling payment complete won't. + } + + if ( $subscription->can_be_updated_to( 'active' ) ) { $subscription->update_status( 'active' ); } // Store PayPal Details on Order - $this->save_paypal_meta_data( $subscription->get_parent(), $transaction_details ); + $this->save_paypal_meta_data( $parent_order, $transaction_details ); // IPN got here first or PDT will never arrive. Normally PDT would have arrived, so the first IPN would not be the first payment. In case the the first payment is an IPN, we need to make sure to not ignore the second one update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' ); @@ -457,7 +458,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { // Make sure subscription hasn't been linked to a new payment method if ( wcs_get_paypal_id( $subscription ) != $ipn_profile_id ) { - WC_Gateway_Paypal::log( sprintf( 'IPN "recurring_payment_suspended" ignored for subscription %d - PayPal profile ID has changed', $subscription->id ) ); + WC_Gateway_Paypal::log( sprintf( 'IPN "recurring_payment_suspended" ignored for subscription %d - PayPal profile ID has changed', $subscription->get_id() ) ); } else if ( $subscription->has_status( 'active' ) ) { @@ -648,6 +649,22 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { return array( 'order_id' => (int) $order_id, 'order_key' => $order_key ); } + /** + * This function will try to get the parent order, and if not available, will get the last order related to the Subscription. + * + * @param WC_Subscription $subscription The Subscription. + * + * @return WC_Order Parent order or the last related order (renewal) + */ + protected static function get_parent_order_with_fallback( $subscription ) { + $order = $subscription->get_parent(); + if ( ! $order ) { + $order = $subscription->get_last_order( 'all' ); + } + + return $order; + } + /** * Cancel a specific PayPal Standard Subscription Profile with PayPal. * diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-supports.php b/includes/gateways/paypal/includes/class-wcs-paypal-supports.php index 3a88e1d..4e92c16 100755 --- a/includes/gateways/paypal/includes/class-wcs-paypal-supports.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-supports.php @@ -35,6 +35,7 @@ class WCS_PayPal_Supports { 'subscription_amount_changes', 'subscription_date_changes', 'multiple_subscriptions', + 'subscription_payment_method_delayed_change', ); /** diff --git a/includes/libraries/action-scheduler/action-scheduler.php b/includes/libraries/action-scheduler/action-scheduler.php index 28b37f1..ce8fee4 100755 --- a/includes/libraries/action-scheduler/action-scheduler.php +++ b/includes/libraries/action-scheduler/action-scheduler.php @@ -5,7 +5,7 @@ * Description: A robust scheduling library for use in WordPress plugins. * Author: Prospress * Author URI: http://prospress.com/ - * Version: 2.1.1 + * Version: 2.2.1 * License: GPLv3 * * Copyright 2018 Prospress, Inc. (email : freedoms@prospress.com) @@ -25,21 +25,21 @@ * */ -if ( ! function_exists( 'action_scheduler_register_2_dot_1_dot_1' ) ) { +if ( ! function_exists( 'action_scheduler_register_2_dot_2_dot_1' ) ) { if ( ! class_exists( 'ActionScheduler_Versions' ) ) { require_once( 'classes/ActionScheduler_Versions.php' ); add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); } - add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_1_dot_1', 0, 0 ); + add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_2_dot_1', 0, 0 ); - function action_scheduler_register_2_dot_1_dot_1() { + function action_scheduler_register_2_dot_2_dot_1() { $versions = ActionScheduler_Versions::instance(); - $versions->register( '2.1.1', 'action_scheduler_initialize_2_dot_1_dot_1' ); + $versions->register( '2.2.1', 'action_scheduler_initialize_2_dot_2_dot_1' ); } - function action_scheduler_initialize_2_dot_1_dot_1() { + function action_scheduler_initialize_2_dot_2_dot_1() { require_once( 'classes/ActionScheduler.php' ); ActionScheduler::init( __FILE__ ); } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler.php b/includes/libraries/action-scheduler/classes/ActionScheduler.php index 51af2b5..2c5d031 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler.php @@ -85,6 +85,11 @@ abstract class ActionScheduler { self::$plugin_file = $plugin_file; spl_autoload_register( array( __CLASS__, 'autoload' ) ); + /** + * Fires in the early stages of Action Scheduler init hook. + */ + do_action( 'action_scheduler_pre_init' ); + $store = self::store(); add_action( 'init', array( $store, 'init' ), 1, 0 ); diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php index 5259797..e34fb87 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php @@ -57,13 +57,16 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst $action = $this->store->fetch_action( $action_id ); $this->store->log_execution( $action_id ); $action->execute(); - do_action( 'action_scheduler_after_execute', $action_id ); + do_action( 'action_scheduler_after_execute', $action_id, $action ); $this->store->mark_complete( $action_id ); } catch ( Exception $e ) { $this->store->mark_failure( $action_id ); do_action( 'action_scheduler_failed_execution', $action_id, $e ); } - $this->schedule_next_instance( $action ); + + if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) ) { + $this->schedule_next_instance( $action ); + } } /** @@ -86,7 +89,7 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst * @author Jeremy Pry */ protected function run_cleanup() { - $this->cleaner->clean(); + $this->cleaner->clean( 10 * $this->get_time_limit() ); } /** diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php b/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php index b1abf12..91d8b18 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php @@ -30,6 +30,7 @@ class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated { if ( class_exists( 'WooCommerce' ) ) { add_action( 'woocommerce_admin_status_content_action-scheduler', array( $this, 'render_admin_ui' ) ); + add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) ); add_filter( 'woocommerce_admin_status_tabs', array( $this, 'register_system_status_tab' ) ); } @@ -37,6 +38,10 @@ class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated { } } + public function system_status_report() { + $table = new ActionScheduler_wcSystemStatus( ActionScheduler::store() ); + $table->print(); + } /** * Registers action-scheduler into WooCommerce > System status. diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php index 3e7db8b..0441e40 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php @@ -31,6 +31,13 @@ class ActionScheduler_CronSchedule implements ActionScheduler_Schedule { return true; } + /** + * @return string + */ + public function get_recurrence() { + return strval($this->cron); + } + /** * For PHP 5.2 compat, since DateTime objects can't be serialized * @return array diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php index 76068e1..604ad61 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php @@ -36,9 +36,7 @@ class ActionScheduler_IntervalSchedule implements ActionScheduler_Schedule { } /** - * @param DateTime $after - * - * @return DateTime|null + * @return int */ public function interval_in_seconds() { return $this->interval_in_seconds; diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_ListTable.php b/includes/libraries/action-scheduler/classes/ActionScheduler_ListTable.php index ad26de8..95f97d6 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_ListTable.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_ListTable.php @@ -222,9 +222,16 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable { */ protected function get_recurrence( $action ) { $recurrence = $action->get_schedule(); - if ( method_exists( $recurrence, 'interval_in_seconds' ) ) { - return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence->interval_in_seconds() ) ); + if ( $recurrence->is_recurring() ) { + if ( method_exists( $recurrence, 'interval_in_seconds' ) ) { + return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence->interval_in_seconds() ) ); + } + + if ( method_exists( $recurrence, 'get_recurrence' ) ) { + return sprintf( __( 'Cron %s', 'action-scheduler' ), $recurrence->get_recurrence() ); + } } + return __( 'Non-repeating', 'action-scheduler' ); } @@ -280,7 +287,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable { protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) { $date = $log_entry->get_date(); $date->setTimezone( $timezone ); - return sprintf( '

  • %s
    %s
  • ', esc_html( $date->format( 'Y-m-d H:i:s e' ) ), esc_html( $log_entry->get_message() ) ); + return sprintf( '
  • %s
    %s
  • ', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) ); } /** @@ -378,7 +385,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable { $next_timestamp = $schedule->next()->getTimestamp(); - $schedule_display_string .= $schedule->next()->format( 'Y-m-d H:i:s e' ); + $schedule_display_string .= $schedule->next()->format( 'Y-m-d H:i:s O' ); $schedule_display_string .= '
    '; if ( gmdate( 'U' ) > $next_timestamp ) { diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php index 30933ce..3da06ab 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php @@ -73,7 +73,7 @@ abstract class ActionScheduler_Logger { $this->log( $action_id, __( 'action complete', 'action-scheduler' ) ); } - public function log_failed_action( $action_id, \Exception $exception ) { + public function log_failed_action( $action_id, Exception $exception ) { $this->log( $action_id, sprintf( __( 'action failed: %s', 'action-scheduler' ), $exception->getMessage() ) ); } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php index 5daf11b..1da13ab 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php @@ -18,13 +18,6 @@ class ActionScheduler_QueueCleaner { */ private $month_in_seconds = 2678400; - /** - * Five minutes in seconds - * - * @var int - */ - private $five_minutes = 300; - /** * ActionScheduler_QueueCleaner constructor. * @@ -77,8 +70,16 @@ class ActionScheduler_QueueCleaner { } } - public function reset_timeouts() { - $timeout = apply_filters( 'action_scheduler_timeout_period', $this->five_minutes ); + /** + * Unclaim pending actions that have not been run within a given time limit. + * + * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed + * as a parameter is 10x the time limit used for queue processing. + * + * @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes). + */ + public function reset_timeouts( $time_limit = 300 ) { + $timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit ); if ( $timeout < 0 ) { return; } @@ -97,8 +98,17 @@ class ActionScheduler_QueueCleaner { } } - public function mark_failures() { - $timeout = apply_filters( 'action_scheduler_failure_period', $this->five_minutes ); + /** + * Mark actions that have been running for more than a given time limit as failed, based on + * the assumption some uncatachable and unloggable fatal error occurred during processing. + * + * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed + * as a parameter is 10x the time limit used for queue processing. + * + * @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes). + */ + public function mark_failures( $time_limit = 300 ) { + $timeout = apply_filters( 'action_scheduler_failure_period', $time_limit ); if ( $timeout < 0 ) { return; } @@ -119,12 +129,13 @@ class ActionScheduler_QueueCleaner { /** * Do all of the cleaning actions. * + * @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes). * @author Jeremy Pry */ - public function clean() { + public function clean( $time_limit = 300 ) { $this->delete_old_actions(); - $this->reset_timeouts(); - $this->mark_failures(); + $this->reset_timeouts( $time_limit ); + $this->mark_failures( $time_limit ); } /** diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php index b3f1730..f58b718 100755 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php @@ -27,7 +27,8 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu */ public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) { if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { - throw new Exception( __( 'The ' . __CLASS__ . ' class can only be run within WP CLI.', 'action-scheduler' ) ); + /* translators: %s php class name */ + throw new Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), __CLASS__ ) ); } parent::__construct( $store, $monitor, $cleaner ); @@ -76,7 +77,7 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu */ protected function add_hooks() { add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) ); - add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ) ); + add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ), 10, 2 ); add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 ); } @@ -143,11 +144,16 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu * * @author Jeremy Pry * - * @param $action_id + * @param int $action_id + * @param null|ActionScheduler_Action $action The instance of the action. Default to null for backward compatibility. */ - public function after_execute( $action_id ) { + public function after_execute( $action_id, $action = null ) { + // backward compatibility + if ( null === $action ) { + $action = $this->store->fetch_action( $action_id ); + } /* translators: %s refers to the action ID */ - WP_CLI::log( sprintf( __( 'Completed processing action %s', 'action-scheduler' ), $action_id ) ); + WP_CLI::log( sprintf( __( 'Completed processing action %s with hook: %s', 'action-scheduler' ), $action_id, $action->get_hook() ) ); } /** diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php b/includes/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php new file mode 100755 index 0000000..ec7f5a4 --- /dev/null +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_wcSystemStatus.php @@ -0,0 +1,129 @@ +store = $store; + } + + /** + * Display action data, including number of actions grouped by status and the oldest & newest action in each status. + * + * Helpful to identify issues, like a clogged queue. + */ + public function print() { + $action_counts = $this->store->action_counts(); + $status_labels = $this->store->get_status_labels(); + $oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) ); + + $this->get_template( $status_labels, $action_counts, $oldest_and_newest ); + } + + /** + * Get oldest and newest scheduled dates for a given set of statuses. + * + * @param array $status_keys Set of statuses to find oldest & newest action for. + * @return array + */ + protected function get_oldest_and_newest( $status_keys ) { + + $oldest_and_newest = array(); + + foreach ( $status_keys as $status ) { + $oldest_and_newest[ $status ] = array( + 'oldest' => '–', + 'newest' => '–', + ); + + if ( 'in-progress' === $status ) { + continue; + } + + $oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' ); + $oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' ); + } + + return $oldest_and_newest; + } + + /** + * Get oldest or newest scheduled date for a given status. + * + * @param string $status Action status label/name string. + * @param string $date_type Oldest or Newest. + * @return DateTime + */ + protected function get_action_status_date( $status, $date_type = 'oldest' ) { + + $order = 'oldest' === $date_type ? 'ASC' : 'DESC'; + + $action = $this->store->query_actions( array( + 'claimed' => false, + 'status' => $status, + 'per_page' => 1, + 'order' => $order, + ) ); + + if ( ! empty( $action ) ) { + $date_object = $this->store->get_date( $action[0] ); + $action_date = $date_object->format( 'Y-m-d H:i:s O' ); + } else { + $action_date = '–'; + } + + return $action_date; + } + + /** + * Get oldest or newest scheduled date for a given status. + * + * @param array $status_labels Set of statuses to find oldest & newest action for. + * @param array $action_counts Number of actions grouped by status. + * @param array $oldest_and_newest Date of the oldest and newest action with each status. + */ + protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) { + ?> + + + + + + + + + + + + + + + + $count ) { + // WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column. + printf( + '', + esc_html( $status_labels[ $status ] ), + number_format_i18n( $count ), + $oldest_and_newest[ $status ]['oldest'], + $oldest_and_newest[ $status ]['newest'] + ); + } + ?> + +

     
    %1$s %2$s, Oldest: %3$s, Newest: %4$s%3$s%4$s
    + + ID, self::SCHEDULE_META_KEY, true ); - if ( empty($schedule) ) { + if ( empty( $schedule ) || ! is_a( $schedule, 'ActionScheduler_Schedule' ) ) { $schedule = new ActionScheduler_NullSchedule(); } $group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array('fields' => 'names') ); @@ -404,7 +441,9 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store { throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'action-scheduler'), $action_id)); } do_action( 'action_scheduler_canceled_action', $action_id ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); wp_trash_post($action_id); + remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); } public function delete_action( $action_id ) { @@ -587,16 +626,9 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store { 'ID' => 'ASC', ), 'date_query' => array( - 'column' => 'post_date', - array( - 'compare' => '<=', - 'year' => $date->format( 'Y' ), - 'month' => $date->format( 'n' ), - 'day' => $date->format( 'j' ), - 'hour' => $date->format( 'G' ), - 'minute' => $date->format( 'i' ), - 'second' => $date->format( 's' ), - ), + 'column' => 'post_date_gmt', + 'before' => $date->format( 'Y-m-d H:i' ), + 'inclusive' => true, ), 'tax_query' => array( array( @@ -685,7 +717,7 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store { $status = $this->get_post_column( $action_id, 'post_status' ); if ( $status === null ) { - throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); + throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); } return $this->get_action_status_by_post_status( $status ); @@ -716,11 +748,13 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store { throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'action-scheduler'), $action_id)); } add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); $result = wp_update_post(array( 'ID' => $action_id, 'post_status' => 'publish', ), TRUE); remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); + remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); if ( is_wp_error($result) ) { throw new RuntimeException($result->get_error_message()); } diff --git a/includes/libraries/action-scheduler/docs/CNAME b/includes/libraries/action-scheduler/docs/CNAME new file mode 100755 index 0000000..3b480b5 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/CNAME @@ -0,0 +1 @@ +actionscheduler.org \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/_config.yml b/includes/libraries/action-scheduler/docs/_config.yml new file mode 100755 index 0000000..fe521d9 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/_config.yml @@ -0,0 +1,7 @@ +title: Action Scheduler - Job Queue for WordPress +description: A scalable, traceable job queue for background processing large queues of tasks in WordPress. Designed for distribution in WordPress plugins - no server access required. +theme: jekyll-theme-hacker +permalink: /:slug/ +plugins: + - jekyll-seo-tag + - jekyll-sitemap diff --git a/includes/libraries/action-scheduler/docs/_layouts/default.html b/includes/libraries/action-scheduler/docs/_layouts/default.html new file mode 100755 index 0000000..8e8a9c5 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/_layouts/default.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + +{% seo %} + + + + +
    +
    +

    Usage | Admin | WP-CLI | Background Processing at Scale | API | FAQ +

    action-scheduler

    +

    A scalable, traceable job queue for background processing large queues of tasks in WordPress. Designed for distribution in WordPress plugins - no server access required.

    +
    +
    + +
    +
    + {{ content }} +
    +
    + + + + {% if site.google_analytics %} + + {% endif %} + + \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/admin.md b/includes/libraries/action-scheduler/docs/admin.md new file mode 100755 index 0000000..5ef0b56 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/admin.md @@ -0,0 +1,22 @@ +--- +description: Learn how to administer background jobs with the Action Scheduler job queue for WordPress. +--- +# Scheduled Actions Administration Screen + +Action Scheduler has a built in administration screen for monitoring, debugging and manually triggering scheduled actions. + +The administration interface is accesible through both: + +1. **Tools > Scheduled Actions** +1. **WooCommerce > Status > Scheduled Actions**, when WooCommerce is installed. + +Among other tasks, from the admin screen you can: + +* run a pending action +* view the scheduled actions with a specific status, like the all actions which have failed or are in-progress (https://cldup.com/NNTwE88Xl8.png). +* view the log entries for a specific action to find out why it failed. +* sort scheduled actions by hook name, scheduled date, claim ID or group name. + +Still have questions? Check out the [FAQ](/faq). + +![](https://cldup.com/5BA2BNB1sw.png) diff --git a/includes/libraries/action-scheduler/docs/android-chrome-192x192.png b/includes/libraries/action-scheduler/docs/android-chrome-192x192.png new file mode 100755 index 0000000..86753db Binary files /dev/null and b/includes/libraries/action-scheduler/docs/android-chrome-192x192.png differ diff --git a/includes/libraries/action-scheduler/docs/android-chrome-256x256.png b/includes/libraries/action-scheduler/docs/android-chrome-256x256.png new file mode 100755 index 0000000..3e5f9b1 Binary files /dev/null and b/includes/libraries/action-scheduler/docs/android-chrome-256x256.png differ diff --git a/includes/libraries/action-scheduler/docs/api.md b/includes/libraries/action-scheduler/docs/api.md new file mode 100755 index 0000000..a0bbb5d --- /dev/null +++ b/includes/libraries/action-scheduler/docs/api.md @@ -0,0 +1,179 @@ +--- +description: Reference guide for background processing functions provided by the Action Scheduler job queue for WordPress. +--- +# API Reference + +Action Scheduler provides a range of functions for scheduling hooks to run at some time in the future on one or more occassions. + +To understand the scheduling functoins, it can help to think of them as extensions to WordPress' `do_action()` function that add the ability to delay and repeat when the hook will be triggered. + +## WP-Cron APIs vs. Action Scheduler APIs + +The Action Scheduler API functions are designed to mirror the WordPress [WP-Cron API functions](http://codex.wordpress.org/Category:WP-Cron_Functions). + +Functions return similar values and accept similar arguments to their WP-Cron counterparts. The notable differences are: + +* `as_schedule_single_action()` & `as_schedule_recurring_action()` will return the post ID of the scheduled action rather than boolean indicating whether the event was scheduled +* `as_schedule_recurring_action()` takes an interval in seconds as the recurring interval rather than an arbitrary string +* `as_schedule_single_action()` & `as_schedule_recurring_action()` can accept a `$group` parameter to group different actions for the one plugin together. +* the `wp_` prefix is substituted with `as_` and the term `event` is replaced with `action` + +## API Function Availability + +As mentioned in the [Usage - Load Order](/usage/#load-order) section, Action Scheduler will initialize itself on the `'init'` hook with priority `1`. While API functions are loaded prior to this and call be called, they should not be called until after `'init'` with priority `1`, because each component, like the data store, has not yet been initialized. + +Do not use Action Scheduler API functions prior to `'init'` hook with priority `1`. Doing so could lead to unexpected results, like data being stored in the incorrect location. + +## Function Reference / `as_schedule_single_action()` + +### Description + +Schedule an action to run one time. + +### Usage + +```php +as_schedule_single_action( $timestamp, $hook, $args, $group ) +```` + +### Parameters + +- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_. +- **$hook** (string)(required) Name of the action hook. Default: _none_. +- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. +- **$group** (array) The group to assign this job to. Default: _''_. + +### Return value + +(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table. + + +## Function Reference / `as_schedule_recurring_action()` + +### Description + +Schedule an action to run repeatedly with a specified interval in seconds. + +### Usage + +```php +as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group ) +```` + +### Parameters + +- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_. +- **$interval_in_seconds** (integer)(required) How long to wait between runs. Default: _none_. +- **$hook** (string)(required) Name of the action hook. Default: _none_. +- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. +- **$group** (array) The group to assign this job to. Default: _''_. + +### Return value + +(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table. + + +## Function Reference / `as_schedule_cron_action()` + +### Description + +Schedule an action that recurs on a cron-like schedule. + +### Usage + +```php +as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group ) +```` + +### Parameters + +- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_. +- **$schedule** (string)(required) $schedule A cron-link schedule string, see http://en.wikipedia.org/wiki/Cron. Default: _none_. +- **$hook** (string)(required) Name of the action hook. Default: _none_. +- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. +- **$group** (array) The group to assign this job to. Default: _''_. + +### Return value + +(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table. + + +## Function Reference / `as_unschedule_action()` + +### Description + +Cancel the next occurrence of a job. + +### Usage + +```php +as_unschedule_action( $hook, $args, $group ) +```` + +### Parameters + +- **$hook** (string)(required) Name of the action hook. Default: _none_. +- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. +- **$group** (array) The group to assign this job to. Default: _''_. + +### Return value + +(null) + + +## Function Reference / `as_next_scheduled_action()` + +### Description + +Returns the next timestamp for a scheduled action. + +### Usage + +```php +as_next_scheduled_action( $hook, $args, $group ) +```` + +### Parameters + +- **$hook** (string)(required) Name of the action hook. Default: _none_. +- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. +- **$group** (array) The group to assign this job to. Default: _''_. + +### Return value + +(integer|boolean) The timestamp for the next occurrence, or false if nothing was found. + + +## Function Reference / `as_get_scheduled_actions()` + +### Description + +Find scheduled actions. + +### Usage + +```php +as_get_scheduled_actions( $args, $return_format ) +```` + +### Parameters + +- **$args** (array) Arguments to search and filter results by. Possible arguments, with their default values: + * `'hook' => ''` - the name of the action that will be triggered + * `'args' => NULL` - the args array that will be passed with the action + * `'date' => NULL` - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). + * `'date_compare' => '<=`' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' + * `'modified' => NULL` - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). + * `'modified_compare' => '<='` - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' + * `'group' => ''` - the group the action belongs to + * `'status' => ''` - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING + * `'claimed' => NULL` - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID + * `'per_page' => 5` - Number of results to return + * `'offset' => 0` + * `'orderby' => 'date'` - accepted values are 'hook', 'group', 'modified', or 'date' + * `'order' => 'ASC'` +- **$return_format** (string) The format in which to return the scheduled actions: 'OBJECT', 'ARRAY_A', or 'ids'. Default: _'OBJECT'_. + +### Return value + +(array) Array of the actions matching the criteria specified with `$args`. diff --git a/includes/libraries/action-scheduler/docs/apple-touch-icon.png b/includes/libraries/action-scheduler/docs/apple-touch-icon.png new file mode 100755 index 0000000..1bc4975 Binary files /dev/null and b/includes/libraries/action-scheduler/docs/apple-touch-icon.png differ diff --git a/includes/libraries/action-scheduler/docs/assets/css/style.scss b/includes/libraries/action-scheduler/docs/assets/css/style.scss new file mode 100755 index 0000000..67fe6b6 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/assets/css/style.scss @@ -0,0 +1,32 @@ +--- +--- + +@import "{{ site.theme }}"; + +a { + text-shadow: none; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +header h1 a { + color: #b5e853; +} + +.container { + max-width: 700px; +} + +footer { + margin-top: 6em; + padding: 1.6em 0; + border-top: 1px dashed #b5e853; +} + +.footer-image { + text-align: center; + padding-top: 1em; +} \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/browserconfig.xml b/includes/libraries/action-scheduler/docs/browserconfig.xml new file mode 100755 index 0000000..f6244e6 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #151515 + + + diff --git a/includes/libraries/action-scheduler/docs/faq.md b/includes/libraries/action-scheduler/docs/faq.md new file mode 100755 index 0000000..38f07f5 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/faq.md @@ -0,0 +1,101 @@ +## FAQ + +### Is it safe to release Action Scheduler in my plugin? Won't its functions conflict with another copy of the library? + +Action Scheduler is designed to be used and released in plugins. It avoids redeclaring public API functions when more than one copy of the library is being loaded by different plugins. It will also load only the most recent version of itself (by checking registered versions after all plugins are loaded on the `'plugins_loaded'` hook). + +To use it in your plugin, simply require the `action-scheduler/action-scheduler.php` file. Action Scheduler will take care of the rest. + +### I don't want to use WP-Cron. Does Action Scheduler depend on WP-Cron? + +By default, Action Scheduler is initiated by WP-Cron. However, it has no dependency on the WP-Cron system. You can initiate the Action Scheduler queue in other ways with just one or two lines of code. + +For example, you can start a queue directly by calling: + +```php +ActionScheduler::runner()->run(); +``` + +Or trigger the `'action_scheduler_run_queue'` hook and let Action Scheduler do it for you: + +```php +do_action( 'action_scheduler_run_queue' ); +``` + +Further customization can be done by extending the `ActionScheduler_Abstract_QueueRunner` class to create a custom Queue Runner. For an example of a customized queue runner, see the [`ActionScheduler_WPCLI_QueueRunner`](https://github.com/Prospress/action-scheduler/blob/master/classes/ActionScheduler_WPCLI_QueueRunner.php), which is used when running WP CLI. + +Want to create some other method for initiating Action Scheduler? [Open a new issue](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it. + +### I don't want to use WP-Cron, ever. Does Action Scheduler replace WP-Cron? + +By default, Action Scheduler is designed to work alongside WP-Cron and not change any of its behaviour. This helps avoid unexpectedly overriding WP-Cron on sites installing your plugin, which may have nothing to do with WP-Cron. + +However, we can understand why you might want to replace WP-Cron completely in environments within you control, especially as it gets you the advantages of Action Scheduler. This should be possible without too much code. + +You could use the `'schedule_event'` hook in WordPress to use Action Scheduler for only newly scheduled WP-Cron jobs and map the `$event` param to Action Scheduler API functions. + +Alternatively, you can use a combination of the `'pre_update_option_cron'` and `'pre_option_cron'` hooks to override all new and previously scheduled WP-Cron jobs (similar to the way [Cavalcade](https://github.com/humanmade/Cavalcade) does it). + +If you'd like to create a plugin to do this automatically and want to share your work with others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it. + +### Eww gross, Custom Post Types! That's _so_ 2010. Can I use a different storage scheme? + +Of course! Action Scheduler data storage is completely swappable, and always has been. + +You can store scheduled actions in custom tables in the WordPress site's database. Some sites using it already are. You can actually store them anywhere for that matter, like in a remote storage service from Amazon Web Services. + +To implement a custom store: + +1. extend the abstract `ActionScheduler_Store` class, being careful to implement each of its methods +2. attach a callback to `'action_scheduler_store_class'` to tell Action Scheduler your class is the one which should be used to manage storage, e.g. + +``` +function eg_define_custom_store( $existing_storage_class ) { + return 'My_Radical_Action_Scheduler_Store'; +} +add_filter( 'action_scheduler_store_class', 'eg_define_custom_store', 10, 1 ); +``` + +Take a look at the `ActionScheduler_wpPostStore` class for an example implementation of `ActionScheduler_Store`. + +If you'd like to create a plugin to do this automatically and release it publicly to help others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it. + +> Note: we're also moving Action Scheduler itself to use [custom tables for better scalability](https://github.com/Prospress/action-scheduler/issues/77). + +### Can I use a different storage scheme just for logging? + +Of course! Action Scheduler's logger is completely swappable, and always has been. You can also customise where logs are stored, and the storage mechanism. + +To implement a custom logger: + +1. extend the abstract `ActionScheduler_Logger` class, being careful to implement each of its methods +2. attach a callback to `'action_scheduler_logger_class'` to tell Action Scheduler your class is the one which should be used to manage logging, e.g. + +``` +function eg_define_custom_logger( $existing_storage_class ) { + return 'My_Radical_Action_Scheduler_Logger'; +} +add_filter( 'action_scheduler_logger_class', 'eg_define_custom_logger', 10, 1 ); +``` + +Take a look at the `ActionScheduler_wpCommentLogger` class for an example implementation of `ActionScheduler_Logger`. + +### I want to run Action Scheduler only on a dedicated application server in my cluster. Can I do that? + +Wow, now you're really asking the tough questions. In theory, yes, this is possible. The `ActionScheduler_QueueRunner` class, which is responsible for running queues, is swappable via the `'action_scheduler_queue_runner_class'` filter. + +Because of this, you can effectively customise queue running however you need. Whether that means tweaking minor things, like not using WP-Cron at all to initiate queues by overriding `ActionScheduler_QueueRunner::init()`, or completely changing how and where queues are run, by overriding `ActionScheduler_QueueRunner::run()`. + +### Is Action Scheduler safe to use on my production site? + +Yes, absolutely! Action Scheduler is actively used on tens of thousands of production sites already. Right now it's responsible for scheduling everything from emails to payments. + +In fact, every month, Action Scheduler processes millions of payments as part of the [WooCommerce Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/) extension. + +It requires no setup, and won't override any WordPress APIs (unless you want it to). + +### How does Action Scheduler work on WordPress Multisite? + +Action Scheduler is designed to manage the scheduled actions on a single site. It has no special handling for running queues across multiple sites in a multisite network. That said, because it's storage and Queue Runner are completely swappable, it would be possible to write multisite handling classes to use with it. + +If you'd like to create a multisite plugin to do this and release it publicly to help others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it. diff --git a/includes/libraries/action-scheduler/docs/favicon-16x16.png b/includes/libraries/action-scheduler/docs/favicon-16x16.png new file mode 100755 index 0000000..aa772d1 Binary files /dev/null and b/includes/libraries/action-scheduler/docs/favicon-16x16.png differ diff --git a/includes/libraries/action-scheduler/docs/favicon-32x32.png b/includes/libraries/action-scheduler/docs/favicon-32x32.png new file mode 100755 index 0000000..773fb30 Binary files /dev/null and b/includes/libraries/action-scheduler/docs/favicon-32x32.png differ diff --git a/includes/libraries/action-scheduler/docs/favicon.ico b/includes/libraries/action-scheduler/docs/favicon.ico new file mode 100755 index 0000000..3cdbb51 Binary files /dev/null and b/includes/libraries/action-scheduler/docs/favicon.ico differ diff --git a/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html b/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html new file mode 100755 index 0000000..f3bf171 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html @@ -0,0 +1 @@ +google-site-verification: google14ef723abb376cd3.html \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/index.md b/includes/libraries/action-scheduler/docs/index.md new file mode 100755 index 0000000..989f262 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/index.md @@ -0,0 +1,68 @@ +--- +title: Action Scheduler - Background Processing Job Queue for WordPress +--- +## WordPress Job Queue with Background Processing + +Action Scheduler is a library for triggering a WordPress hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occassions. + +Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. + +It just so happens, this functionality also creates a robust job queue for background processing large queues of tasks in WordPress. With the additional of logging and an [administration interface](/admin/), that also provide tracability on your tasks processed in the background. + +### Battle-Tested Background Processing + +Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins. + +It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, in 10 concurrent queues at a rate of over 10,000 actions / hour without negatively impacting normal site operations. + +This is all possible on infrastructure and WordPress sites outside the control of the plugin author. + +Action Scheduler is specifically designed for distribution in WordPress plugins (and themes) - no server access required. If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help. + +### How it Works + +Action Scheduler uses a WordPress [custom post type](http://codex.wordpress.org/Post_Types), creatively named `scheduled-action`, to store the hook name, arguments and scheduled date for an action that should be triggered at some time in the future. + +The scheduler will attempt to run every minute by attaching itself as a callback to the `'action_scheduler_run_schedule'` hook, which is scheduled using WordPress's built-in [WP-Cron](http://codex.wordpress.org/Function_Reference/wp_cron) system. + +When triggered, Action Scheduler will check for posts of the `scheduled-action` type that have a `post_date` at or before this point in time i.e. actions scheduled to run now or at sometime in the past. + +### Batch Processing Background Jobs + +If there are actions to be processed, Action Scheduler will stake a unique claim for a batch of 20 actions and begin processing that batch. The PHP process spawned to run the batch will then continue processing batches of 20 actions until it times out or exhausts available memory. + +If your site has a large number of actions scheduled to run at the same time, Action Scheduler will process more than one batch at a time. Specifically, when the `'action_scheduler_run_schedule'` hook is triggered approximately one minute after the first batch began processing, a new PHP process will stake a new claim to a batch of actions which were not claimed by the previous process. It will then begin to process that batch. + +This will continue until all actions are processed using a maximum of 5 concurrent queues. + +### Housekeeping + +Before processing a batch, the scheduler will remove any existing claims on actions which have been sitting in a queue for more than five minutes. + +Action Scheduler will also trash any actions which were completed more than a month ago. + +If an action runs for more than 5 minutes, Action Scheduler will assume the action has timed out and will mark it as failed. However, if all callbacks attached to the action were to successfully complete sometime after that 5 minute timeout, its status would later be updated to completed. + +### Traceable Background Processing + +Did your background job run? + +Never be left wondering with Action Scheduler's built-in record keeping. + +All events for each action are logged in the [comments table](http://codex.wordpress.org/Database_Description#Table_Overview) and displayed in the [administration interface](/admin/). + +The events logged by default include when an action: + * is created + * starts + * completes + * fails + +If it fails with an error that can be recorded, that error will be recorded in the log and visible in administration interface, making it possible to trace what went wrong at some point in the past on a site you didn't have access to in the past. + +Actions can also be grouped together using a custom taxonomy named `action-group`. + +## Credits + +Developed and maintained by [Prospress](http://prospress.com/) in collaboration with [Flightless](https://flightless.us/). + +Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/prospress/action-scheduler/pulls) welcome. \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/mstile-150x150.png b/includes/libraries/action-scheduler/docs/mstile-150x150.png new file mode 100755 index 0000000..3a3ed19 Binary files /dev/null and b/includes/libraries/action-scheduler/docs/mstile-150x150.png differ diff --git a/includes/libraries/action-scheduler/docs/perf.md b/includes/libraries/action-scheduler/docs/perf.md new file mode 100755 index 0000000..542792d --- /dev/null +++ b/includes/libraries/action-scheduler/docs/perf.md @@ -0,0 +1,127 @@ +--- +title: WordPress Background Processing at Scale - Action Scheduler Job Queue +description: Learn how to do WordPress background processing at scale by tuning the Action Scheduler job queue's default WP Cron runner. +--- +# Background Processing at Scale + +Action Scheduler's default processing is designed to work reliably across all different hosting environments. In order to achieve that, the default processing thresholds are very conservative. + +Specifically, Action Scheduler will only process actions until: + +* 90% of available memory is used +* processing another 3 actions would exceed 30 seconds of total request time, based on the average processing time for the current batch + +On sites with large queues, this can result in very slow processing time. + +While using [WP CLI to process queues](/wp-cli/) is the best approach to increasing processing speed, on occasion, that is not a viable option. In these cases, it's also possible to increase the processing thresholds in Action Scheduler to increase the rate at which actions are processed by the default WP Cron queue runner. + +## Increasing Time Limit + +By default, Action Scheduler will only process actions for a maximum of 30 seconds. This time limit minimises the risk of a script timeout on unknown hosting environments, some of which enforce 30 second timeouts. + +If you know your host supports longer than this time limit for web requests, you can increase this time limit. This allows more actions to be processed in each request and reduces the lag between processing each queue, greating speeding up the processing rate of scheduled actions. + +For example, the following snippet will increase the timelimit to 2 minutes (120 seconds): + +```php +function eg_increase_time_limit( $time_limit ) { + return 120; +} +add_filter( 'action_scheduler_queue_runner_time_limit', 'eg_increase_time_limit' ); +``` + +Some of the known host time limits are: + +* 60 second on WP Engine +* 120 seconds on Pantheon +* 120 seconds on SiteGround + +## Increasing Batch Size + +By default, Action Scheduler will claim a batch of 25 actions. This small batch size is because the default time limit is only 30 seconds; however, if you know your actions are processing very quickly, e.g. taking microseconds not seconds, or that you have more than 30 second available to process each batch, increasing the batch size can improve performance. + +This is because claiming a batch has some overhead, so the less often a batch needs to be claimed, the faster actions can be processed. + +For example, to increase the batch size to 100, we can use the following function: + +```php +function eg_increase_action_scheduler_batch_size( $batch_size ) { + return 100; +} +add_filter( 'action_scheduler_queue_runner_batch_size', 'eg_increase_action_scheduler_batch_size' ); +``` + +## Increasing Concurrent Batches + +By default, Action Scheduler will run up to 5 concurrent batches of actions. This is to prevent consuming all the available connections or processes on your webserver. + +However, your server may allow a large number of connection, for example, because it has a high value for Apache's `MaxClients` setting or PHP-FPM's `pm.max_children` setting. + +If this is the case, you can use the `'action_scheduler_queue_runner_concurrent_batches'` filter to increase the number of conncurrent batches allowed, and therefore speed up processing large numbers of actions scheduled to be processed simultaneously. + +For example, to increase the allowed number of concurrent queues to 10, we can use the following code: + +```php +function eg_increase_action_scheduler_concurrent_batches( $concurrent_batches ) { + return 10; +} +add_filter( 'action_scheduler_queue_runner_concurrent_batches', 'eg_increase_action_scheduler_concurrent_batches' ); +``` + +## Increasing Initialisation Rate of Runners + +By default, Action scheduler initiates at most, one queue runner every time the `'action_scheduler_run_queue'` action is triggered by WP Cron. + +Because this action is only triggered at most once every minute, if a queue is only allowed to process for one minute, then there will never be more than one queue processing actions, greatly reducing the processing rate. + +To handle larger queues on more powerful servers, it's a good idea to initiate additional queue runners whenever the `'action_scheduler_run_queue'` action is run. + +That can be done by initiated additional secure requests to our server via loopback requests. + +The code below demonstrates how to create 5 loopback requests each time a queue begins + +```php +/** + * Trigger 5 additional loopback requests with unique URL params. + */ +function eg_request_additional_runners() { + + // allow self-signed SSL certificates + add_filter( 'https_local_ssl_verify', '__return_false', 100 ); + + for ( $i = 0; $i < 5; $i++ ) { + $response = wp_remote_post( admin_url( 'admin-ajax.php' ), array( + 'method' => 'POST', + 'timeout' => 45, + 'redirection' => 5, + 'httpversion' => '1.0', + 'blocking' => false, + 'headers' => array(), + 'body' => array( + 'action' => 'eg_create_additional_runners', + 'instance' => $i, + 'eg_nonce' => wp_create_nonce( 'eg_additional_runner_' . $i ), + ), + 'cookies' => array(), + ) ); + } +} +add_action( 'action_scheduler_run_queue', 'eg_request_additional_runners', 0 ); + +/** + * Handle requests initiated by eg_request_additional_runners() and start a queue runner if the request is valid. + */ +function eg_create_additional_runners() { + + if ( isset( $_POST['eg_nonce'] ) && isset( $_POST['instance'] ) && wp_verify_nonce( $_POST['eg_nonce'], 'eg_additional_runner_' . $_POST['instance'] ) ) { + ActionScheduler_QueueRunner::instance()->run(); + } + + wp_die(); +} +add_action( 'wp_ajax_nopriv_eg_create_additional_runners', 'eg_create_additional_runners', 0 ); +``` + +## High Volume Plugin + +It's not necessary to add all of this code yourself, the folks at [Prospress](https://prospress.com) have created a handy plugin to get access to each of these increases - the [Action Scheduler - High Volume](https://github.com/prospress/action-scheduler-high-volume) plugin. diff --git a/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg b/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg new file mode 100755 index 0000000..b67c32b --- /dev/null +++ b/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg @@ -0,0 +1,40 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/includes/libraries/action-scheduler/docs/site.webmanifest b/includes/libraries/action-scheduler/docs/site.webmanifest new file mode 100755 index 0000000..de65106 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/includes/libraries/action-scheduler/docs/usage.md b/includes/libraries/action-scheduler/docs/usage.md new file mode 100755 index 0000000..dda4ed7 --- /dev/null +++ b/includes/libraries/action-scheduler/docs/usage.md @@ -0,0 +1,123 @@ +--- +description: Learn how to use the Action Scheduler background processing job queue for WordPress in your WordPress plugin. +--- +# Usage + +Using Action Scheduler requires: + +1. installing the library +1. scheduling and action +1. attaching a callback to that action + +## Scheduling an Action + +To schedule an action, call the [API function](/api/) for the desired schedule type passing in the required parameters. + +The example code below shows everything needed to schedule a function to run at midnight, if it's not already scheduled: + +```php +require_once( plugin_dir_path( __FILE__ ) . '/libraries/action-scheduler/action-scheduler.php' ); + +/** + * Schedule an action with the hook 'eg_midnight_log' to run at midnight each day + * so that our callback is run then. + */ +function eg_log_action_data() { + if ( false === as_next_scheduled_action( 'eg_midnight_log' ) ) { + as_schedule_recurring_action( strtotime( 'midnight tonight' ), DAY_IN_SECONDS, 'eg_midnight_log' ); + } +} +add_action( 'init', 'eg_log_action_data' ); + +/** + * A callback to run when the 'eg_midnight_log' scheduled action is run. + */ +function eg_log_action_data() { + error_log( 'It is just after midnight on ' . date( 'Y-m-d' ) ); +} +add_action( 'eg_midnight_log', 'eg_log_action_data' ); +``` + +For more details on all available API functions, and the data they accept, refer to the [API Reference](/api/). + +## Installation + +There are two ways to install Action Scheduler: + +1. regular WordPress plugin; or +1. a library within your plugin's codebase. + +### Usage as a Plugin + +Action Scheduler includes the necessary file headers to be used as a standard WordPress plugin. + +To install it as a plugin: + +1. Download the .zip archive of the latest [stable release](https://github.com/Prospress/action-scheduler/releases) +1. Go to the **Plugins > Add New > Upload** administration screen on your WordPress site +1. Select the archive file you just downloaded +1. Click **Install Now** +1. Click **Activate** + +Or clone the Git repository into your site's `wp-content/plugins` folder. + +Using Action Scheduler as a plugin can be handy for developing against newer versions, rather than having to update the subtree in your codebase. **When installed as a plugin, Action Scheduler does not provide any user interfaces for scheduling actions**. The only way to interact with Action Scheduler is via code. + +### Usage as a Library + +To use Action Scheduler as a library: + +1. include the Action Scheduler codebase +1. load the library by including the `action-scheduler.php` file + +Using a [subtree in your plugin, theme or site's Git repository](https://www.atlassian.com/blog/git/alternatives-to-git-submodule-git-subtree) to include Action Scheduler is the recommended method. Composer can also be used. + +To include Action Scheduler as a git subtree: + +#### Step 1. Add the Repository as a Remote + +``` +git remote add -f subtree-action-scheduler https://github.com/Prospress/action-scheduler.git +``` + +Adding the subtree as a remote allows us to refer to it in short from via the name `subtree-action-scheduler`, instead of the full GitHub URL. + +#### Step 2. Add the Repo as a Subtree + +``` +git subtree add --prefix libraries/action-scheduler subtree-action-scheduler master --squash +``` + +This will add the `master` branch of Action Scheduler to your repository in the folder `libraries/action-scheduler`. + +You can change the `--prefix` to change where the code is included. Or change the `master` branch to a tag, like `2.1.0` to include only a stable version. + +#### Step 3. Update the Subtree + +To update Action Scheduler to a new version, use the commands: + +``` +git fetch subtree-action-scheduler master +git subtree pull --prefix libraries/action-scheduler subtree-action-scheduler master --squash +``` + +### Loading Action Scheduler + +Regardless of how it is installed, to load Action Scheduler, you only need to include the `action-scheduler.php` file, e.g. + +```php + $batch_size, - 'subscription_status' => wcs_get_subscription_ended_statuses(), + 'subscription_status' => $subscription_ended_statuses, 'meta_query' => array( array( 'key' => '_schedule_end', diff --git a/includes/upgrades/class-wc-subscriptions-upgrader.php b/includes/upgrades/class-wc-subscriptions-upgrader.php index fa54ae9..fe69a7d 100755 --- a/includes/upgrades/class-wc-subscriptions-upgrader.php +++ b/includes/upgrades/class-wc-subscriptions-upgrader.php @@ -101,9 +101,6 @@ class WC_Subscriptions_Upgrader { // When WC is updated from a version prior to 3.0 to a version after 3.0, add subscription address indexes. Must be hooked on before WC runs its updates, which occur on priority 5. add_action( 'init', array( __CLASS__, 'maybe_add_subscription_address_indexes' ), 2 ); - // Hooks into WC's wc_update_350_order_customer_id upgrade routine. - add_action( 'init', array( __CLASS__, 'maybe_update_subscription_post_author' ), 2 ); - add_action( 'admin_notices', array( __CLASS__, 'maybe_add_downgrade_notice' ) ); add_action( 'admin_notices', array( __CLASS__, 'maybe_display_external_object_cache_warning' ) ); @@ -238,6 +235,11 @@ class WC_Subscriptions_Upgrader { self::$background_updaters['2.4']['start_date_metadata']->schedule_repair(); } + // Upon upgrading or installing 2.5.0 for the first time, enable or disable PayPal Standard for Subscriptions. + if ( version_compare( self::$active_version, '2.5.0', '<' ) ) { + WCS_PayPal::set_enabled_for_subscriptions_default(); + } + self::upgrade_complete(); } @@ -368,8 +370,8 @@ class WC_Subscriptions_Upgrader { $results = array( 'upgraded_count' => 0, - // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag - 'message' => sprintf( __( 'Unable to upgrade subscriptions.
    Error: %1$s
    Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '' ), + // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, 4$: break tag + 'message' => sprintf( __( 'Unable to upgrade subscriptions.%4$sError: %1$s%4$sPlease refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '', '
    ' ), 'status' => 'error', ); } @@ -415,8 +417,8 @@ class WC_Subscriptions_Upgrader { $results = array( 'repaired_count' => 0, 'unrepaired_count' => 0, - // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag - 'message' => sprintf( _x( 'Unable to repair subscriptions.
    Error: %1$s
    Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'Error message that gets sent to front end when upgrading Subscriptions', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '' ), + // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, 4$: break tag + 'message' => sprintf( _x( 'Unable to repair subscriptions.%4$sError: %1$s%4$sPlease refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'Error message that gets sent to front end when upgrading Subscriptions', 'woocommerce-subscriptions' ), '' . $e->getMessage(). '', '', '', '
    ' ), 'status' => 'error', ); } @@ -826,24 +828,6 @@ class WC_Subscriptions_Upgrader { } } - /** - * Handles the WC 3.5.0 upgrade routine that moves customer IDs from post metadata to the 'post_author' column. - * - * @since 2.4.0 - */ - public static function maybe_update_subscription_post_author() { - if ( version_compare( WC()->version, '3.5.0', '<' ) ) { - return; - } - - // If WC hasn't run the update routine yet we can hook into theirs to update subscriptions, otherwise we'll need to schedule our own update. - if ( version_compare( get_option( 'woocommerce_db_version' ), '3.5.0', '<' ) ) { - self::$background_updaters['2.4']['subscription_post_author']->hook_into_wc_350_update(); - } else if ( version_compare( self::$active_version, '2.4.0', '<' ) ) { - self::$background_updaters['2.4']['subscription_post_author']->schedule_repair(); - } - } - /** * Load and initialise the background updaters. * @@ -854,7 +838,6 @@ class WC_Subscriptions_Upgrader { self::$background_updaters['2.3']['suspended_paypal_repair'] = new WCS_Repair_Suspended_PayPal_Subscriptions( $logger ); self::$background_updaters['2.3']['address_indexes_repair'] = new WCS_Repair_Subscription_Address_Indexes( $logger ); self::$background_updaters['2.4']['start_date_metadata'] = new WCS_Repair_Start_Date_Metadata( $logger ); - self::$background_updaters['2.4']['subscription_post_author'] = new WCS_Upgrade_Subscription_Post_Author( $logger ); // Init the updaters foreach ( self::$background_updaters as $version => $updaters ) { @@ -903,6 +886,27 @@ class WC_Subscriptions_Upgrader { $admin_notice->display(); } + /** + * Handles the WC 3.5.0 upgrade routine that moves customer IDs from post metadata to the 'post_author' column. + * + * @since 2.4.0 + * @deprecated 2.5.0 + */ + public static function maybe_update_subscription_post_author() { + wcs_deprecated_function( __METHOD__, '2.5.0' ); + + if ( version_compare( WC()->version, '3.5.0', '<' ) ) { + return; + } + + // If WC hasn't run the update routine yet we can hook into theirs to update subscriptions, otherwise we'll need to schedule our own update. + if ( version_compare( get_option( 'woocommerce_db_version' ), '3.5.0', '<' ) ) { + self::$background_updaters['2.4']['subscription_post_author']->hook_into_wc_350_update(); + } else if ( version_compare( self::$active_version, '2.4.0', '<' ) ) { + self::$background_updaters['2.4']['subscription_post_author']->schedule_repair(); + } + } + /** * Used to check if a user ID is greater than the last user upgraded to version 1.4. * diff --git a/includes/upgrades/class-wcs-upgrade-logger.php b/includes/upgrades/class-wcs-upgrade-logger.php index 61132fd..2191803 100755 --- a/includes/upgrades/class-wcs-upgrade-logger.php +++ b/includes/upgrades/class-wcs-upgrade-logger.php @@ -25,7 +25,8 @@ class WCS_Upgrade_Logger { public static function init() { - add_action( 'woocommerce_subscriptions_upgraded', __CLASS__ . '::schedule_cleanup', 10, 2 ); + add_action( 'woocommerce_subscriptions_upgraded', array( __CLASS__, 'schedule_cleanup' ), 10, 2 ); + add_action( 'woocommerce_subscriptions_upgraded', array( __CLASS__, 'add_more_info' ), 10 ); } /** @@ -63,6 +64,34 @@ class WCS_Upgrade_Logger { } } + /** + * Log more information during upgrade: Information about environment and active plugins + * + * @since 2.5.0 + */ + public static function add_more_info() { + global $wp_version; + + self::add( sprintf( 'Environment info:' ) ); + self::add( sprintf( ' WordPress Version : %s', $wp_version ) ); + + $active_plugins = get_option( 'active_plugins' ); + + // Check if get_plugins() function exists. This is required on the front end of the site. + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $all_plugins = get_plugins(); + self::add( sprintf( 'Active Plugins:' ) ); + + foreach ( $active_plugins as $plugin ) { + $author = empty( $all_plugins[ $plugin ]['Author'] ) ? 'Unknown' : $all_plugins[ $plugin ]['Author']; + $version = empty( $all_plugins[ $plugin ]['Version'] ) ? 'Unknown version' : $all_plugins[ $plugin ]['Version']; + self::add( sprintf( ' %s by %s – %s', $all_plugins[ $plugin ]['Name'], $author, $version ) ); + } + } + /** * Schedule a hook to automatically clear the log after 8 weeks */ @@ -71,3 +100,4 @@ class WCS_Upgrade_Logger { self::add( sprintf( '%s upgrade complete from Subscriptions v%s while WooCommerce WC_VERSION %s and database version %s was active.', $current_version, $old_version, $wc_version, get_option( 'woocommerce_db_version' ) ) ); } } + diff --git a/includes/upgrades/class-wcs-upgrade-notice-manager.php b/includes/upgrades/class-wcs-upgrade-notice-manager.php index c143f54..4f4fdd7 100755 --- a/includes/upgrades/class-wcs-upgrade-notice-manager.php +++ b/includes/upgrades/class-wcs-upgrade-notice-manager.php @@ -19,7 +19,7 @@ class WCS_Upgrade_Notice_Manager { * * @var string */ - protected static $version = '2.3.0'; + protected static $version = '2.5.0'; /** * The number of times the notice will be displayed before being dismissed automatically. @@ -77,26 +77,25 @@ class WCS_Upgrade_Notice_Manager { return; } - $version = _x( '2.3', 'plugin version number used in admin notice', 'woocommerce-subscriptions' ); + $version = _x( '2.5', 'plugin version number used in admin notice', 'woocommerce-subscriptions' ); $dismiss_url = wp_nonce_url( add_query_arg( 'dismiss_upgrade_notice', self::$version ), 'dismiss_upgrade_notice', '_wcsnonce' ); $notice = new WCS_Admin_Notice( 'notice notice-info', array(), $dismiss_url ); $features = array( array( - 'title' => __( 'New Subscription Coupon Features', 'woocommerce-subscriptions' ), - 'description' => __( 'Want to offer customers coupons which apply for 6 months? You can now define the number of cycles discounts would be applied.', 'woocommerce-subscriptions' ), + 'title' => __( 'New options to allow customers to sign up without a credit card', 'woocommerce-subscriptions' ), + 'description' => __( 'Allow customers to access free trial and other $0 subscription products without needing to enter their credit card details on sign up.', 'woocommerce-subscriptions' ), ), array( - 'title' => __( 'New Signup Pricing Options for Synchronized Subscriptions', 'woocommerce-subscriptions' ), - 'description' => __( 'Charge the full recurring price at the time of sign up for synchronized subscriptions. Your customers can now receive their products straight away.', 'woocommerce-subscriptions' ), + 'title' => __( 'Improved subscription payment method information', 'woocommerce-subscriptions' ), + 'description' => __( 'Customers can now see more information about what payment method will be used for future payments.', 'woocommerce-subscriptions' ), ), array( - 'title' => __( 'Link Parent Orders to Subscriptions', 'woocommerce-subscriptions' ), - // translators: placeholders are opening and closing tags linking to documentation. - 'description' => sprintf( __( 'For subscriptions with no parent order, shop managers can now choose a parent order via the Edit Subscription screen. This makes it possible to set a parent order on %smanually created subscriptions%s. The order can also be sent to customers to act as an invoice that needs to be paid to activate the subscription.', 'woocommerce-subscriptions' ), '', '' ), + 'title' => __( 'Auto-renewal toggle', 'woocommerce-subscriptions' ), + 'description' => sprintf( __( 'Enabled via a setting, this new feature will allow your customers to turn on and off automatic payments from the %sMy Account > View Subscription%s pages.', 'woocommerce-subscriptions' ), '', '' ), ), array( - 'title' => __( 'Early Renewal', 'woocommerce-subscriptions' ), - 'description' => __( 'Customers can now renew their subscriptions before the scheduled next payment date. Why not use this to email your customers a coupon a month before their annual subscription renewals to get access to that revenue sooner?', 'woocommerce-subscriptions' ), + 'title' => __( 'Update all subscription payment methods', 'woocommerce-subscriptions' ), + 'description' => __( "Customers will now have the option to update all their subscriptions when they are changing one of their subscription's payment methods - provided the payment gateway supports it.", 'woocommerce-subscriptions' ), ), ); @@ -109,7 +108,7 @@ class WCS_Upgrade_Notice_Manager { $notice->set_actions( array( array( 'name' => __( 'Learn More', 'woocommerce-subscriptions' ), - 'url' => 'https://docs.woocommerce.com/document/subscriptions/version-2-3/', + 'url' => 'https://docs.woocommerce.com/document/subscriptions/version-2-5/', ), ) ); diff --git a/includes/upgrades/class-wcs-upgrade-subscription-post-author.php b/includes/upgrades/class-wcs-upgrade-subscription-post-author.php index 89c3a9c..7446f75 100755 --- a/includes/upgrades/class-wcs-upgrade-subscription-post-author.php +++ b/includes/upgrades/class-wcs-upgrade-subscription-post-author.php @@ -2,10 +2,11 @@ /** * Updates the 'post_author' column for subscriptions on WC 3.5+. * - * @author Prospress - * @category Admin - * @package WooCommerce Subscriptions/Admin/Upgrades - * @version 2.4.0 + * @author Prospress + * @category Admin + * @package WooCommerce Subscriptions/Admin/Upgrades + * @version 2.4.0 + * @deprecated 2.5.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -21,6 +22,8 @@ class WCS_Upgrade_Subscription_Post_Author extends WCS_Background_Upgrader { * @since 2.4.0 */ public function __construct( WC_Logger $logger ) { + wcs_deprecated_function( __METHOD__, '2.5.0' ); + $this->scheduled_hook = 'wcs_upgrade_subscription_post_author'; $this->log_handle = 'wcs-upgrade-subscription-post-author'; $this->logger = $logger; diff --git a/includes/upgrades/templates/wcs-upgrade.php b/includes/upgrades/templates/wcs-upgrade.php index dd4730f..0d94ae3 100755 --- a/includes/upgrades/templates/wcs-upgrade.php +++ b/includes/upgrades/templates/wcs-upgrade.php @@ -40,7 +40,7 @@ if ( ! defined( 'ABSPATH' ) ) {

    -

    +

    @@ -50,7 +50,7 @@ if ( ! defined( 'ABSPATH' ) ) {

    20 ) : ?> -

    +

    diff --git a/includes/wcs-cart-functions.php b/includes/wcs-cart-functions.php index f263017..128b2ff 100755 --- a/includes/wcs-cart-functions.php +++ b/includes/wcs-cart-functions.php @@ -192,7 +192,7 @@ function wcs_cart_totals_shipping_method_price_label( $method, $cart ) { $price_label .= _x( 'Free', 'shipping method price', 'woocommerce-subscriptions' ); } - return $price_label; + return apply_filters( 'wcs_cart_totals_shipping_method_price_label', $price_label, $method, $cart ); } /** @@ -305,7 +305,7 @@ function wcs_cart_price_string( $recurring_amount, $cart ) { 'subscription_interval' => wcs_cart_pluck( $cart, 'subscription_period_interval' ), 'subscription_period' => wcs_cart_pluck( $cart, 'subscription_period', '' ), 'subscription_length' => wcs_cart_pluck( $cart, 'subscription_length' ), - ) ) ); + ), $cart ) ); } /** diff --git a/includes/wcs-compatibility-functions.php b/includes/wcs-compatibility-functions.php index 4ca95b8..41b0c76 100755 --- a/includes/wcs-compatibility-functions.php +++ b/includes/wcs-compatibility-functions.php @@ -54,39 +54,36 @@ function wcs_help_tip( $tip, $allow_html = false ) { * returned as MySQL strings in the site's timezone. We return them from here as MySQL strings in UTC timezone because that's how * dates are used in Subscriptions in almost all cases, for sanity's sake. * - * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access. - * @param string $property The property name. - * @param string $single Whether to return just the first piece of meta data with the given property key, or all meta data. - * @param mixed $default (optional) The value to return if no value is found - defaults to single -> null, multiple -> array() + * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access. + * @param string $property The property name. + * @param string $single Whether to return just the first piece of meta data with the given property key, or all meta data. + * @param mixed $default (optional) The value to return if no value is found - defaults to single -> null, multiple -> array(). + * * @since 2.2.0 * @return mixed */ function wcs_get_objects_property( $object, $property, $single = 'single', $default = null ) { + $prefixed_key = wcs_maybe_prefix_key( $property ); + $value = ! is_null( $default ) ? $default : ( ( 'single' === $single ) ? null : array() ); + $property_function_map = array( + 'order_version' => 'version', + 'order_currency' => 'currency', + 'order_date' => 'date_created', + 'date' => 'date_created', + 'cart_discount' => 'total_discount', + ); - $prefixed_key = wcs_maybe_prefix_key( $property ); - - $value = ! is_null( $default ) ? $default : ( ( 'single' == $single ) ? null : array() ); + if ( isset( $property_function_map[ $property ] ) ) { + $property = $property_function_map[ $property ]; + } switch ( $property ) { - - case 'name' : // the replacement for post_title added in 3.0 - if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { - $value = $object->post->post_title; - } else { // WC 3.0+ - $value = $object->get_name(); - } - break; - case 'post' : - if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { - $value = $object->post; - } else { // WC 3.0+ - // In order to keep backwards compatibility it's required to use the parent data for variations. - if ( method_exists( $object, 'is_type' ) && $object->is_type( 'variation' ) ) { - $value = get_post( $object->get_parent_id() ); - } else { - $value = get_post( $object->get_id() ); - } + // In order to keep backwards compatibility it's required to use the parent data for variations. + if ( method_exists( $object, 'is_type' ) && $object->is_type( 'variation' ) ) { + $value = get_post( wcs_get_objects_property( $object, 'parent_id' ) ); + } else { + $value = get_post( wcs_get_objects_property( $object, 'id' ) ); } break; @@ -94,113 +91,32 @@ function wcs_get_objects_property( $object, $property, $single = 'single', $defa $value = wcs_get_objects_property( $object, 'post' )->post_status; break; - case 'parent_id' : - if ( method_exists( $object, 'get_parent_id' ) ) { // WC 3.0+ or an instance of WC_Product_Subscription_Variation_Legacy with WC < 3.0 - $value = $object->get_parent_id(); - } else { // WC 2.1-2.6 - $value = $object->get_parent(); - } - break; - case 'variation_data' : - if ( function_exists( 'wc_get_product_variation_attributes' ) ) { // WC 3.0+ - $value = wc_get_product_variation_attributes( $object->get_id() ); - } else { - $value = $object->$property; - } - break; - - case 'downloads' : - if ( method_exists( $object, 'get_downloads' ) ) { // WC 3.0+ - $value = $object->get_downloads(); - } else { - $value = $object->get_files(); - } - break; - - case 'order_version' : - case 'version' : - if ( method_exists( $object, 'get_version' ) ) { // WC 3.0+ - $value = $object->get_version(); - } else { // WC 2.1-2.6 - $value = $object->order_version; - } - break; - - case 'order_currency' : - case 'currency' : - if ( method_exists( $object, 'get_currency' ) ) { // WC 3.0+ - $value = $object->get_currency(); - } else { // WC 2.1-2.6 - $value = $object->get_order_currency(); - } - break; - - // Always return a PHP DateTime object in site timezone (or null), the same thing the WC_Order::get_date_created() method returns in WC 3.0+ to make it easier to migrate away from WC < 3.0 - case 'date_created' : - case 'order_date' : - case 'date' : - if ( method_exists( $object, 'get_date_created' ) ) { // WC 3.0+ - $value = $object->get_date_created(); - } else { - // Base the value off tht GMT value when possible and then set the DateTime's timezone based on the current site's timezone to avoid incorrect values when the timezone has changed - if ( '0000-00-00 00:00:00' != $object->post->post_date_gmt ) { - $value = new WC_DateTime( $object->post->post_date_gmt, new DateTimeZone( 'UTC' ) ); - $value->setTimezone( new DateTimeZone( wc_timezone_string() ) ); - } else { - $value = new WC_DateTime( $object->post->post_date, new DateTimeZone( wc_timezone_string() ) ); - } - } - break; - - // Always return a PHP DateTime object in site timezone (or null), the same thing the getter returns in WC 3.0+ to make it easier to migrate away from WC < 3.0 - case 'date_paid' : - if ( method_exists( $object, 'get_date_paid' ) ) { // WC 3.0+ - $value = $object->get_date_paid(); - } else { - if ( ! empty( $object->paid_date ) ) { - // Because the paid_date post meta value was set in the site timezone at the time it was set, this won't always be correct, but is the best we can do with WC < 3.0 - $value = new WC_DateTime( $object->paid_date, new DateTimeZone( wc_timezone_string() ) ); - } else { - $value = null; - } - } - break; - - case 'cart_discount' : - if ( method_exists( $object, 'get_total_discount' ) ) { // WC 3.0+ - $value = $object->get_total_discount(); - } else { // WC 2.1-2.6 - $value = $object->cart_discount; - } + $value = wc_get_product_variation_attributes( wcs_get_objects_property( $object, 'id' ) ); break; default : - $function_name = 'get_' . $property; if ( is_callable( array( $object, $function_name ) ) ) { $value = $object->$function_name(); } else { - - // If we don't have a method for this specific property, but we are using WC 3.0, it may be set as meta data on the object so check if we can use that - if ( method_exists( $object, 'get_meta' ) ) { - if ( $object->meta_exists( $prefixed_key ) ) { - if ( 'single' === $single ) { - $value = $object->get_meta( $prefixed_key, true ); - } else { - // WC_Data::get_meta() returns an array of stdClass objects with id, key & value properties when meta is available - $value = wp_list_pluck( $object->get_meta( $prefixed_key, false ), 'value' ); - } + // If we don't have a method for this specific property, but we are using WC 3.0, it may be set as meta data on the object so check if we can use that. + if ( $object->meta_exists( $prefixed_key ) ) { + if ( 'single' === $single ) { + $value = $object->get_meta( $prefixed_key, true ); + } else { + // WC_Data::get_meta() returns an array of stdClass objects with id, key & value properties when meta is available. + $value = wp_list_pluck( $object->get_meta( $prefixed_key, false ), 'value' ); } - } elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0 + } elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0. $value = $object->$property; } elseif ( strtolower( $property ) !== 'id' && metadata_exists( 'post', wcs_get_objects_property( $object, 'id' ), $prefixed_key ) ) { - // If we couldn't find a property or function, fallback to using post meta as that's what many __get() methods in WC < 3.0 did + // If we couldn't find a property or function, fallback to using post meta as that's what many __get() methods in WC < 3.0 did. if ( 'single' === $single ) { $value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, true ); } else { - // Get all the meta values + // Get all the meta values. $value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, false ); } } @@ -519,3 +435,48 @@ function wcs_set_coupon_property( &$coupon, $property, $value ) { } } } + +/** + * Generate an order/subscription key. + * + * This is a compatibility wrapper for @see wc_generate_order_key() which was introduced in WC 3.5.4. + * + * @return string $order_key. + * @since 2.5.0 + */ +function wcs_generate_order_key() { + + if ( function_exists( 'wc_generate_order_key' ) ) { + $order_key = wc_generate_order_key(); + } else { + $order_key = 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . wp_generate_password( 13, false ) ); + } + + return $order_key; +} + +/** + * Update a single option for a WC_Settings_API object. + * + * This is a compatibility wrapper for @see WC_Settings_API::update_option() which was introduced in WC 3.4.0. + * + * @param WC_Settings_API $settings_api The object to update the option for. + * @param string $key Option key. + * @param mixed $value Value to set. + * @since 2.5.1 + */ +function wcs_update_settings_option( $settings_api, $key, $value ) { + + // WooCommerce 3.4+ + if ( is_callable( array( $settings_api, 'update_option' ) ) ) { + $settings_api->update_option( $key, $value ); + } else { + if ( empty( $settings_api->settings ) ) { + $settings_api->init_settings(); + } + + $settings_api->settings[ $key ] = $value; + + return update_option( $settings_api->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $settings_api->id, $settings_api->settings ), 'yes' ); + } +} diff --git a/includes/wcs-formatting-functions.php b/includes/wcs-formatting-functions.php index ecf3bd6..560fc6c 100755 --- a/includes/wcs-formatting-functions.php +++ b/includes/wcs-formatting-functions.php @@ -157,7 +157,7 @@ function wcs_price_string( $subscription_details ) { if ( 1 == $subscription_details['subscription_interval'] ) { // e.g. $15 on March 15th each year if ( ! empty( $subscription_details['initial_amount'] ) ) { - // translators: 1$: initial amount, 2$: intial description (e.g. "up front"), 3$: recurring amount, 4$: month of year (e.g. "March"), 5$: day of the month (e.g. "23rd") + // translators: 1$: initial amount, 2$: initial description (e.g. "up front"), 3$: recurring amount, 4$: month of year (e.g. "March"), 5$: day of the month (e.g. "23rd") $subscription_string = sprintf( __( '%1$s %2$s then %3$s on %4$s %5$s each year', 'woocommerce-subscriptions' ), $initial_amount_string, $subscription_details['initial_description'], $recurring_amount_string, $wp_locale->month[ $payment_day['month'] ], WC_Subscriptions::append_numeral_suffix( $payment_day['day'] ) ); } else { // translators: 1$: recurring amount, 2$: month (e.g. "March"), 3$: day of the month (e.g. "23rd") (e.g. "$15 on March 15th every 3rd year") @@ -213,7 +213,7 @@ function wcs_price_string( $subscription_details ) { } /** - * Display a human friendly time diff for a given timestamp, e.g. "In 12 hours" or "12 hours ago". + * Display a human friendly time diff for a given timestamp, e.g. "in 12 hours" or "12 hours ago". * * @param int $timestamp_gmt * @return string A human friendly string to display for the timestamp's date @@ -225,13 +225,15 @@ function wcs_get_human_time_diff( $timestamp_gmt ) { if ( $time_diff > 0 && $time_diff < WEEK_IN_SECONDS ) { // translators: placeholder is human time diff (e.g. "3 weeks") - $date_to_display = sprintf( __( 'In %s', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) ); + $date_to_display = sprintf( __( 'in %s', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) ); } elseif ( $time_diff < 0 && absint( $time_diff ) < WEEK_IN_SECONDS ) { // translators: placeholder is human time diff (e.g. "3 weeks") $date_to_display = sprintf( __( '%s ago', 'woocommerce-subscriptions' ), human_time_diff( current_time( 'timestamp', true ), $timestamp_gmt ) ); } else { $timestamp_site = wcs_date_to_time( get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $timestamp_gmt ) ) ); $date_to_display = date_i18n( wc_date_format(), $timestamp_site ) . ' ' . date_i18n( wc_time_format(), $timestamp_site ); + // translators: placeholder is a localized date and time (e.g. "February 1, 2018 10:20 PM") + $date_to_display = sprintf( _x( '%s', 'wcs_get_human_time_diff', 'woocommerce-subscriptions' ), $date_to_display ); } return $date_to_display; diff --git a/includes/wcs-order-functions.php b/includes/wcs-order-functions.php index e4c4259..8dac055 100755 --- a/includes/wcs-order-functions.php +++ b/includes/wcs-order-functions.php @@ -747,6 +747,8 @@ function wcs_display_item_meta( $item, $order ) { * @return void */ function wcs_display_item_downloads( $item, $order ) { + wcs_deprecated_function( __FUNCTION__, '2.5.0', 'wc_display_item_downloads( $item )' ); + if ( function_exists( 'wc_display_item_downloads' ) ) { // WC 3.0+ wc_display_item_downloads( $item ); } else { @@ -865,4 +867,32 @@ function wcs_copy_payment_method_to_order( $subscription, $order ) { if ( ! empty( $payment_meta ) ) { wcs_set_payment_meta( $order, $payment_meta ); } + +} + +/** + * Returns how many minutes ago the order was created. + * + * @param WC_Order $order + * + * @return int + * @since 2.5.3 + */ +function wcs_minutes_since_order_created( $order ) { + $now = new WC_DateTime( 'now', $order->get_date_created()->getTimezone() ); + $diff_in_minutes = $now->diff( $order->get_date_created() ); + + return absint( $diff_in_minutes->i ); +} + +/** + * Returns how many seconds ago the order was created. + * + * @param WC_Order $order + * + * @return int + * @since 2.5.3 + */ +function wcs_seconds_since_order_created( $order ) { + return time() - $order->get_date_created()->getTimestamp(); } diff --git a/includes/wcs-user-functions.php b/includes/wcs-user-functions.php index 2cd8892..d2bb476 100755 --- a/includes/wcs-user-functions.php +++ b/includes/wcs-user-functions.php @@ -334,7 +334,7 @@ function wcs_get_all_user_actions_for_subscription( $subscription, $user_id ) { ); } - if ( wcs_can_user_resubscribe_to( $subscription, $user_id ) ) { + if ( wcs_can_user_resubscribe_to( $subscription, $user_id ) && false == $subscription->can_be_updated_to( 'active' ) ) { $actions['resubscribe'] = array( 'url' => wcs_get_users_resubscribe_link( $subscription ), 'name' => __( 'Resubscribe', 'woocommerce-subscriptions' ), diff --git a/languages/woocommerce-subscriptions.pot b/languages/woocommerce-subscriptions.pot index b5cd118..9087956 100755 --- a/languages/woocommerce-subscriptions.pot +++ b/languages/woocommerce-subscriptions.pot @@ -1,92 +1,100 @@ -# Copyright (C) 2018 Prospress Inc. +# Copyright (C) 2019 Prospress Inc. # This file is distributed under the same license as the WooCommerce Subscriptions package. msgid "" msgstr "" -"Project-Id-Version: WooCommerce Subscriptions 2.4.7\n" +"Project-Id-Version: WooCommerce Subscriptions 2.5.3\n" "Report-Msgid-Bugs-To: " "https://github.com/Prospress/woocommerce-subscriptions/issues\n" -"POT-Creation-Date: 2018-12-21 11:20:12+00:00\n" +"POT-Creation-Date: 2019-03-20 06:30:32+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2018-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2019-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: Prospress Translations \n" "X-Generator: grunt-wp-i18n 0.5.4\n" "Language: en_US\n" -#: includes/abstracts/abstract-wcs-related-order-store.php:138 +#: includes/abstracts/abstract-wcs-related-order-store.php:147 msgid "Invalid relation type: %s. Order relationship type must be one of: %s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:185 +#: includes/admin/class-wc-subscriptions-admin.php:191 msgid "Simple subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:186 +#: includes/admin/class-wc-subscriptions-admin.php:192 msgid "Variable subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:204 +#: includes/admin/class-wc-subscriptions-admin.php:213 +msgid "Downloadable" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:214 +msgid "Virtual" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:278 msgid "Choose the subscription price, billing interval and period." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:217 +#: includes/admin/class-wc-subscriptions-admin.php:291 #: templates/admin/html-variation-price.php:44 #. translators: placeholder is a currency symbol / code msgid "Subscription price (%s)" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:220 +#: includes/admin/class-wc-subscriptions-admin.php:294 msgid "Subscription interval" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:226 -#: includes/admin/class-wc-subscriptions-admin.php:362 +#: includes/admin/class-wc-subscriptions-admin.php:300 +#: includes/admin/class-wc-subscriptions-admin.php:437 msgid "Subscription period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:240 -#: includes/admin/class-wc-subscriptions-admin.php:363 +#: includes/admin/class-wc-subscriptions-admin.php:314 +#: includes/admin/class-wc-subscriptions-admin.php:438 #: templates/admin/html-variation-price.php:66 msgid "Expire after" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:243 +#: includes/admin/class-wc-subscriptions-admin.php:317 msgid "" "Automatically expire the subscription after this length of time. This " "length is in addition to any free trial or amount of time provided before a " "synchronised first renewal date." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:252 +#: includes/admin/class-wc-subscriptions-admin.php:327 #: templates/admin/html-variation-price.php:20 #. translators: %s is a currency symbol / code msgid "Sign-up fee (%s)" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:254 +#: includes/admin/class-wc-subscriptions-admin.php:329 msgid "" "Optionally include an amount to be charged at the outset of the " "subscription. The sign-up fee will be charged immediately, even if the " "product has a free trial or the payment dates are synced." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:266 +#: includes/admin/class-wc-subscriptions-admin.php:341 #: templates/admin/html-variation-price.php:25 msgid "Free trial" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:269 +#: includes/admin/class-wc-subscriptions-admin.php:344 #: templates/admin/deprecated/html-variation-price.php:115 msgid "Subscription Trial Period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:301 +#: includes/admin/class-wc-subscriptions-admin.php:376 msgid "One time shipping" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:302 +#: includes/admin/class-wc-subscriptions-admin.php:377 msgid "" "Shipping for subscription products is normally charged on the initial order " "and all renewal orders. Enable this to only charge shipping once on the " @@ -94,63 +102,63 @@ msgid "" "not have a free trial or a synced renewal date." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:359 +#: includes/admin/class-wc-subscriptions-admin.php:434 msgid "Subscription pricing" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:360 +#: includes/admin/class-wc-subscriptions-admin.php:435 msgid "Subscription sign-up fee" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:361 +#: includes/admin/class-wc-subscriptions-admin.php:436 msgid "Subscription billing interval" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:364 +#: includes/admin/class-wc-subscriptions-admin.php:439 msgid "Free trial length" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:365 +#: includes/admin/class-wc-subscriptions-admin.php:440 msgid "Free trial period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:686 +#: includes/admin/class-wc-subscriptions-admin.php:764 msgid "" "Unable to change subscription status to \"%s\". Please assign a customer to " "the subscription to activate it." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:728 +#: includes/admin/class-wc-subscriptions-admin.php:806 msgid "" "Trashing this order will also trash the subscriptions purchased with the " "order." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:741 +#: includes/admin/class-wc-subscriptions-admin.php:819 msgid "Enter the new period, either day, week, month or year:" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:742 +#: includes/admin/class-wc-subscriptions-admin.php:820 msgid "Enter a new length (e.g. 5):" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:743 +#: includes/admin/class-wc-subscriptions-admin.php:821 msgid "" "Enter a new interval as a single number (e.g. to charge every 2nd month, " "enter 2):" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:744 +#: includes/admin/class-wc-subscriptions-admin.php:822 msgid "Delete all variations without a subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:747 +#: includes/admin/class-wc-subscriptions-admin.php:825 msgid "" "Product type can not be changed because this product is associated with " "active subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:751 +#: includes/admin/class-wc-subscriptions-admin.php:829 msgid "" "You are about to trash one or more orders which contain a subscription.\n" "\n" @@ -158,7 +166,7 @@ msgid "" "orders." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:764 +#: includes/admin/class-wc-subscriptions-admin.php:842 msgid "" "WARNING: Bad things are about to happen!\n" "\n" @@ -170,13 +178,13 @@ msgid "" "gateway." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:765 +#: includes/admin/class-wc-subscriptions-admin.php:843 msgid "" "You are deleting a subscription item. You will also need to manually cancel " "and trash the subscription on the Manage Subscriptions screen." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:772 +#: includes/admin/class-wc-subscriptions-admin.php:850 msgid "" "Warning: Deleting a user will also delete the user's subscriptions. The " "user's orders will remain but be reassigned to the 'Guest' user.\n" @@ -185,63 +193,75 @@ msgid "" "subscriptions?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:835 +#: includes/admin/class-wc-subscriptions-admin.php:854 +msgid "" +"PayPal Standard has a number of limitations and does not support all " +"subscription features." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:854 +msgid "" +"Because of this, it is not recommended as a payment method for " +"Subscriptions unless it is the only available option for your country." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:917 msgid "Active subscriber?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:878 +#: includes/admin/class-wc-subscriptions-admin.php:960 msgid "Manage Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:882 -#: woocommerce-subscriptions.php:262 +#: includes/admin/class-wc-subscriptions-admin.php:964 +#: woocommerce-subscriptions.php:263 msgid "Search Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:902 -#: includes/admin/class-wc-subscriptions-admin.php:1017 +#: includes/admin/class-wc-subscriptions-admin.php:984 +#: includes/admin/class-wc-subscriptions-admin.php:1099 #: includes/admin/class-wcs-admin-reports.php:46 #: includes/admin/class-wcs-admin-system-status.php:56 #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:688 -#: includes/class-wcs-query.php:108 includes/class-wcs-query.php:129 -#: includes/class-wcs-query.php:248 +#: includes/class-wcs-query.php:115 includes/class-wcs-query.php:142 +#: includes/class-wcs-query.php:296 #: includes/privacy/class-wcs-privacy-exporters.php:51 -#: woocommerce-subscriptions.php:253 woocommerce-subscriptions.php:266 +#: woocommerce-subscriptions.php:254 woocommerce-subscriptions.php:267 msgid "Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1057 +#: includes/admin/class-wc-subscriptions-admin.php:1139 msgid "Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1064 +#: includes/admin/class-wc-subscriptions-admin.php:1146 msgid "Add to Cart Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1065 +#: includes/admin/class-wc-subscriptions-admin.php:1147 msgid "" "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." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1069 -#: includes/admin/class-wc-subscriptions-admin.php:1072 -#: includes/admin/class-wc-subscriptions-admin.php:1081 -#: includes/admin/class-wc-subscriptions-admin.php:1084 +#: includes/admin/class-wc-subscriptions-admin.php:1151 +#: includes/admin/class-wc-subscriptions-admin.php:1154 +#: includes/admin/class-wc-subscriptions-admin.php:1163 +#: includes/admin/class-wc-subscriptions-admin.php:1166 #: includes/class-wc-product-subscription-variation.php:98 #: includes/class-wc-product-subscription.php:72 #: includes/class-wc-product-variable-subscription.php:73 #: includes/class-wc-subscriptions-product.php:99 -#: woocommerce-subscriptions.php:560 +#: woocommerce-subscriptions.php:578 msgid "Sign Up Now" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1076 +#: includes/admin/class-wc-subscriptions-admin.php:1158 msgid "Place Order Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1077 +#: includes/admin/class-wc-subscriptions-admin.php:1159 msgid "" "Use this field to customise the text displayed on the checkout button when " "an order contains a subscription. Normally the checkout submission button " @@ -249,11 +269,11 @@ msgid "" "changed to \"Sign Up Now\"." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1090 +#: includes/admin/class-wc-subscriptions-admin.php:1172 msgid "Roles" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1093 +#: includes/admin/class-wc-subscriptions-admin.php:1175 #. translators: placeholders are tags msgid "" "Choose the default roles to assign to active and inactive subscribers. For " @@ -262,46 +282,46 @@ msgid "" "allocated these roles to prevent locking out administrators." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1098 +#: includes/admin/class-wc-subscriptions-admin.php:1180 msgid "Subscriber Default Role" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1099 +#: includes/admin/class-wc-subscriptions-admin.php:1181 msgid "" "When a subscription is activated, either manually or after a successful " "purchase, new users will be assigned this role." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1110 +#: includes/admin/class-wc-subscriptions-admin.php:1192 msgid "Inactive Subscriber Role" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1111 +#: includes/admin/class-wc-subscriptions-admin.php:1193 msgid "" "If a subscriber's subscription is manually cancelled or expires, she will " "be assigned this role." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1131 +#: includes/admin/class-wc-subscriptions-admin.php:1213 msgid "Manual Renewal Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1132 +#: includes/admin/class-wc-subscriptions-admin.php:1214 msgid "Accept Manual Renewals" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1137 +#: includes/admin/class-wc-subscriptions-admin.php:1219 #. translators: placeholders are opening and closing link tags msgid "" "With manual renewals, a customer's subscription is put on-hold until they " "login and pay to renew it. %sLearn more%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1143 +#: includes/admin/class-wc-subscriptions-admin.php:1225 msgid "Turn off Automatic Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1148 +#: includes/admin/class-wc-subscriptions-admin.php:1230 #. translators: placeholders are opening and closing link tags msgid "" "If you don't want new subscription purchases to automatically charge " @@ -310,11 +330,11 @@ msgid "" "more%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1163 +#: includes/admin/class-wc-subscriptions-admin.php:1245 msgid "Customer Suspensions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1170 +#: includes/admin/class-wc-subscriptions-admin.php:1252 msgid "" "Set a maximum number of times a customer can suspend their account for each " "billing period. For example, for a value of 3 and a subscription billed " @@ -324,30 +344,46 @@ msgid "" "this to 0 to turn off the customer suspension feature completely." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1174 +#: includes/admin/class-wc-subscriptions-admin.php:1256 msgid "Mixed Checkout" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1175 +#: includes/admin/class-wc-subscriptions-admin.php:1257 msgid "Allow multiple subscriptions and products to be purchased simultaneously." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1179 +#: includes/admin/class-wc-subscriptions-admin.php:1261 msgid "" "Allow a subscription product to be purchased with other products and " "subscriptions in the same transaction." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1183 +#: includes/admin/class-wc-subscriptions-admin.php:1265 +msgid "$0 Initial Checkout" +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1266 +msgid "Allow $0 initial checkout without a payment method." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1270 +msgid "" +"Allow a subscription product with a $0 initial payment to be purchased " +"without providing a payment method. The customer will be required to " +"provide a payment method at the end of the initial period to keep the " +"subscription active." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1274 #: includes/upgrades/templates/wcs-about-2-0.php:108 msgid "Drip Downloadable Content" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1184 +#: includes/admin/class-wc-subscriptions-admin.php:1275 msgid "Enable dripping for downloadable content on subscription products." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1188 +#: includes/admin/class-wc-subscriptions-admin.php:1279 msgid "" "Enabling this grants access to new downloadable files added to a product " "only after the next renewal is processed.%sBy default, access to new " @@ -355,7 +391,7 @@ msgid "" "customer that has an active subscription with that product." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1224 +#: includes/admin/class-wc-subscriptions-admin.php:1315 #. translators: $1-$2: opening and closing tags, $3-$4: opening and #. closing tags msgid "" @@ -363,73 +399,77 @@ msgid "" "start selling subscriptions!%4$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1229 +#: includes/admin/class-wc-subscriptions-admin.php:1320 msgid "Add a Subscription Product" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1230 +#: includes/admin/class-wc-subscriptions-admin.php:1321 #: includes/upgrades/templates/wcs-about-2-0.php:35 #: includes/upgrades/templates/wcs-about.php:34 -#: woocommerce-subscriptions.php:1091 +#: woocommerce-subscriptions.php:1110 msgid "Settings" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1316 +#: includes/admin/class-wc-subscriptions-admin.php:1405 #. translators: placeholder is a number msgid "We can't find a subscription with ID #%d. Perhaps it was deleted?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1349 -#: includes/admin/class-wc-subscriptions-admin.php:1354 +#: includes/admin/class-wc-subscriptions-admin.php:1445 +msgid "We can't find a paid subscription order for this user." +msgstr "" + +#: includes/admin/class-wc-subscriptions-admin.php:1477 +#: includes/admin/class-wc-subscriptions-admin.php:1482 #. translators: placeholders are opening link tag, ID of sub, and closing link #. tag msgid "Showing orders for %sSubscription %s%s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1378 +#: includes/admin/class-wc-subscriptions-admin.php:1506 #. translators: number of 1$: days, 2$: weeks, 3$: months, 4$: years msgid "The trial period can not exceed: %1s, %2s, %3s or %4s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1383 +#: includes/admin/class-wc-subscriptions-admin.php:1511 #. translators: placeholder is a time period (e.g. "4 weeks") msgid "The trial period can not exceed %s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1446 -#: includes/admin/class-wc-subscriptions-admin.php:1499 +#: includes/admin/class-wc-subscriptions-admin.php:1574 +#: includes/admin/class-wc-subscriptions-admin.php:1641 #: includes/admin/class-wcs-admin-system-status.php:95 -#: includes/admin/reports/class-wcs-report-cache-manager.php:328 +#: includes/admin/reports/class-wcs-report-cache-manager.php:331 msgid "Yes" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1446 +#: includes/admin/class-wc-subscriptions-admin.php:1574 #: includes/admin/class-wcs-admin-system-status.php:95 -#: includes/admin/reports/class-wcs-report-cache-manager.php:328 +#: includes/admin/reports/class-wcs-report-cache-manager.php:331 msgid "No" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1482 +#: includes/admin/class-wc-subscriptions-admin.php:1610 msgid "Automatic Recurring Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1499 +#: includes/admin/class-wc-subscriptions-admin.php:1641 msgid "" "Supports automatic renewal payments with the WooCommerce Subscriptions " "extension." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1579 +#: includes/admin/class-wc-subscriptions-admin.php:1736 msgid "Subscription items can no longer be edited." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1583 +#: includes/admin/class-wc-subscriptions-admin.php:1740 msgid "" "This subscription is no longer editable because the payment gateway does " "not allow modification of recurring amounts." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1602 +#: includes/admin/class-wc-subscriptions-admin.php:1759 #. translators: $1-2: opening and closing tags of a link that takes to Woo #. marketplace / Stripe product page msgid "" @@ -438,18 +478,18 @@ msgid "" "the %1$sfree Stripe extension%2$s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1607 +#: includes/admin/class-wc-subscriptions-admin.php:1764 msgid "Recurring Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1615 +#: includes/admin/class-wc-subscriptions-admin.php:1772 #. translators: placeholders are opening and closing link tags msgid "" "Payment gateways which don't support automatic recurring payments can be " "used to process %smanual subscription renewal payments%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1622 +#: includes/admin/class-wc-subscriptions-admin.php:1779 #. translators: $1-$2: opening and closing tags. Link to documents->payment #. gateways, 3$-4$: opening and closing tags. Link to WooCommerce extensions #. shop page @@ -458,7 +498,7 @@ msgid "" "the official %3$sWooCommerce Marketplace%4$s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1724 +#: includes/admin/class-wc-subscriptions-admin.php:1881 msgid "Note that purchasing a subscription still requires an account." msgstr "" @@ -561,10 +601,10 @@ msgstr[1] "" #: templates/myaccount/my-subscriptions.php:26 #: templates/myaccount/my-subscriptions.php:41 #: templates/myaccount/related-orders.php:24 -#: templates/myaccount/related-orders.php:45 +#: templates/myaccount/related-orders.php:50 #: templates/myaccount/related-subscriptions.php:21 #: templates/myaccount/related-subscriptions.php:35 -#: templates/myaccount/subscription-details.php:18 +#: templates/myaccount/subscription-details.php:17 msgid "Status" msgstr "" @@ -576,7 +616,7 @@ msgstr "" #: templates/emails/subscription-info.php:18 #: templates/myaccount/my-subscriptions.php:25 #: templates/myaccount/related-subscriptions.php:20 -#: woocommerce-subscriptions.php:254 +#: woocommerce-subscriptions.php:255 msgid "Subscription" msgstr "" @@ -629,12 +669,12 @@ msgid "Delete Permanently" msgstr "" #: includes/admin/class-wcs-admin-post-types.php:487 -#: includes/class-wc-subscriptions-product.php:746 +#: includes/class-wc-subscriptions-product.php:748 msgid "Restore this item from the Trash" msgstr "" #: includes/admin/class-wcs-admin-post-types.php:487 -#: includes/class-wc-subscriptions-product.php:747 +#: includes/class-wc-subscriptions-product.php:749 msgid "Restore" msgstr "" @@ -671,7 +711,7 @@ msgstr[0] "" msgstr[1] "" #: includes/admin/class-wcs-admin-post-types.php:601 -#: templates/myaccount/my-subscriptions.php:49 +#: includes/class-wc-subscription.php:1949 #. translators: placeholder is the display name of a payment gateway a #. subscription was paid by msgid "Via %s" @@ -687,51 +727,57 @@ msgid "" "this subscription controls when payments are processed." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:884 -#: includes/admin/class-wcs-admin-post-types.php:887 -#: includes/admin/class-wcs-admin-post-types.php:890 +#: includes/admin/class-wcs-admin-post-types.php:893 +#: includes/admin/class-wcs-admin-post-types.php:896 +#: includes/admin/class-wcs-admin-post-types.php:899 msgid "Subscription updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:885 +#: includes/admin/class-wcs-admin-post-types.php:894 msgid "Custom field updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:886 +#: includes/admin/class-wcs-admin-post-types.php:895 msgid "Custom field deleted." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:891 +#: includes/admin/class-wcs-admin-post-types.php:900 msgid "Subscription saved." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:892 +#: includes/admin/class-wcs-admin-post-types.php:901 msgid "Subscription submitted." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:894 +#: includes/admin/class-wcs-admin-post-types.php:903 #. translators: php date string msgid "Subscription scheduled for: %1$s." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:895 +#: includes/admin/class-wcs-admin-post-types.php:904 msgid "Subscription draft updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:931 +#: includes/admin/class-wcs-admin-post-types.php:940 msgid "Any Payment Method" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:932 +#: includes/admin/class-wcs-admin-post-types.php:941 msgid "None" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:1125 +#: includes/admin/class-wcs-admin-post-types.php:947 +#: includes/class-wc-subscription.php:1932 +#: includes/class-wcs-change-payment-method-admin.php:155 +msgid "Manual Renewal" +msgstr "" + +#: includes/admin/class-wcs-admin-post-types.php:1136 #. translators: 1: user display name 2: user ID 3: user email msgid "%1$s (#%2$s – %3$s)" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:1132 +#: includes/admin/class-wcs-admin-post-types.php:1143 #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:84 msgid "Search for a customer…" msgstr "" @@ -761,7 +807,7 @@ msgid "Failed Payment Retries" msgstr "" #: includes/admin/class-wcs-admin-reports.php:104 -#: includes/admin/reports/class-wcs-report-cache-manager.php:274 +#: includes/admin/reports/class-wcs-report-cache-manager.php:277 msgid "WooCommerce" msgstr "" @@ -839,7 +885,7 @@ msgid "Billing Details" msgstr "" #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:134 -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:206 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:222 #: includes/payment-retry/class-wcs-retry-post-store.php:38 msgid "Edit" msgstr "" @@ -850,8 +896,8 @@ msgstr "" #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:142 #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:144 -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:215 -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:217 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:231 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:233 msgid "Address" msgstr "" @@ -866,31 +912,39 @@ msgid "Payment Method" msgstr "" #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:205 -msgid "Shipping Details" +msgid "Customer change payment method page →" msgstr "" #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:207 +msgid "Customer add payment method page →" +msgstr "" + +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:221 +msgid "Shipping Details" +msgstr "" + +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:223 msgid "Copy from billing" msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:208 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:224 msgid "Load shipping address" msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:217 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:233 msgid "No shipping address set." msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:239 -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:269 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:255 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:285 msgid "Customer Provided Note" msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:270 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:286 msgid "Customer's notes about the order" msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:358 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:373 #. translators: placeholder is error message from the payment gateway or #. subscriptions when updating the status msgid "Error updating some information: %s" @@ -902,7 +956,7 @@ msgid "Unpublished" msgstr "" #: includes/admin/meta-boxes/views/html-related-orders-table.php:17 -#: templates/myaccount/related-orders.php:37 +#: templates/myaccount/related-orders.php:42 msgid "Order Number" msgstr "" @@ -912,10 +966,10 @@ msgstr "" #: includes/admin/meta-boxes/views/html-related-orders-table.php:19 #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:549 -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:173 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:190 #: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:204 #: templates/myaccount/related-orders.php:23 -#: templates/myaccount/related-orders.php:42 +#: templates/myaccount/related-orders.php:47 msgid "Date" msgstr "" @@ -986,18 +1040,18 @@ msgstr "" msgid "Error: unable to find timezone of your browser." msgstr "" -#: includes/admin/reports/class-wcs-report-cache-manager.php:277 +#: includes/admin/reports/class-wcs-report-cache-manager.php:280 msgid "" "Please note: data for this report is cached. The data displayed may be out " "of date by up to 24 hours. The cache is updated each morning at 4am in your " "site's timezone." msgstr "" -#: includes/admin/reports/class-wcs-report-cache-manager.php:332 +#: includes/admin/reports/class-wcs-report-cache-manager.php:335 msgid "Cache Update Failures" msgstr "" -#: includes/admin/reports/class-wcs-report-cache-manager.php:335 +#: includes/admin/reports/class-wcs-report-cache-manager.php:338 #. translators: %d refers to the number of times we have detected cache update #. failures msgid "%d failures" @@ -1005,15 +1059,35 @@ msgid_plural "%d failure" msgstr[0] "" msgstr[1] "" -#: includes/admin/reports/class-wcs-report-dashboard.php:78 -msgid "%s signup subscription signups this month" -msgid_plural "%s signups subscription signups this month" +#: includes/admin/reports/class-wcs-report-dashboard.php:212 +#. translators: 1$: count, 2$ and 3$ are opening and closing strong tags, +#. respectively. +msgid "%2$s%$1s signup%3$s subscription signups this month" +msgid_plural "%2$s%1$s signups%3$s subscription signups this month" msgstr[0] "" msgstr[1] "" -#: includes/admin/reports/class-wcs-report-dashboard.php:83 -msgid "%s renewal subscription renewals this month" -msgid_plural "%s renewals subscription renewals this month" +#: includes/admin/reports/class-wcs-report-dashboard.php:218 +msgid "%s signup revenue this month" +msgstr "" + +#: includes/admin/reports/class-wcs-report-dashboard.php:225 +#. translators: 1$: count, 2$ and 3$ are opening and closing strong tags, +#. respectively. +msgid "%2$s%1$s renewal%3$s subscription renewals this month" +msgid_plural "%2$s%1$s renewals%3$s subscription renewals this month" +msgstr[0] "" +msgstr[1] "" + +#: includes/admin/reports/class-wcs-report-dashboard.php:231 +msgid "%s renewal revenue this month" +msgstr "" + +#: includes/admin/reports/class-wcs-report-dashboard.php:238 +#. translators: 1$: count, 2$ and 3$ are opening and closing strong tags, +#. respectively. +msgid "%2$s%1$s cancellation%3$s subscription cancellations this month" +msgid_plural "%2$s%1$s cancellations%3$s subscription cancellations this month" msgstr[0] "" msgstr[1] "" @@ -1042,22 +1116,50 @@ msgstr "" msgid "Total Subscribers" msgstr "" +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:44 +msgid "" +"The number of unique customers with a subscription of any status other than " +"pending or trashed." +msgstr "" + #: includes/admin/reports/class-wcs-report-subscription-by-customer.php:45 msgid "Active Subscriptions" msgstr "" +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:45 +msgid "" +"The total number of subscriptions with a status of active or pending " +"cancellation." +msgstr "" + #: includes/admin/reports/class-wcs-report-subscription-by-customer.php:46 msgid "Total Subscriptions" msgstr "" +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:46 +msgid "" +"The total number of subscriptions with a status other than pending or " +"trashed." +msgstr "" + #: includes/admin/reports/class-wcs-report-subscription-by-customer.php:47 msgid "Total Subscription Orders" msgstr "" +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:47 +msgid "" +"The total number of sign-up, switch and renewal orders placed with your " +"store with a paid status (i.e. processing or complete)." +msgstr "" + #: includes/admin/reports/class-wcs-report-subscription-by-customer.php:48 msgid "Average Lifetime Value" msgstr "" +#: includes/admin/reports/class-wcs-report-subscription-by-customer.php:48 +msgid "The average value of all customers' sign-up, switch and renewal orders." +msgstr "" + #: includes/admin/reports/class-wcs-report-subscription-by-customer.php:96 msgid "Active Subscriptions %s" msgstr "" @@ -1176,7 +1278,8 @@ msgstr "" #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:420 msgid "" "The number of subscriptions created during this period, either by being " -"manually created, imported or a customer placing an order." +"manually created, imported or a customer placing an order. This includes " +"orders pending payment." msgstr "" #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:426 @@ -1260,27 +1363,27 @@ msgid "Change in subscriptions between the start and end of the period." msgstr "" #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:506 -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:137 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:154 msgid "Year" msgstr "" #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:507 -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:138 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:155 msgid "Last Month" msgstr "" #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:508 -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:139 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:156 msgid "This Month" msgstr "" #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:509 -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:140 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:157 msgid "Last 7 Days" msgstr "" #: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:553 -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:177 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:194 #: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:208 msgid "Export CSV" msgstr "" @@ -1325,68 +1428,68 @@ msgstr "" msgid "Renewal Totals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:95 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:112 msgid "%s renewal revenue recovered" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:96 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:113 msgid "" "The total amount of revenue, including tax and shipping, recovered with the " "failed payment retry system for renewal orders with a failed payment." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:102 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:119 #: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:93 msgid "%s renewal orders" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:103 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:120 msgid "" "The number of renewal orders which had a failed payment use the retry " "system." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:108 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:125 msgid "%s retry attempts succeeded" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:109 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:126 msgid "" "The number of renewal payment retries for this period which were able to " "process the payment which had previously failed one or more times." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:115 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:132 msgid "%s retry attempts failed" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:116 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:133 msgid "" "The number of renewal payment retries for this period which did not result " "in a successful payment." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:122 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:139 msgid "%s retry attempts pending" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:123 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:140 msgid "The number of renewal payment retries not yet processed." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:225 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:242 msgid "Successful retries" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:241 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:258 msgid "Failed retries" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:257 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:274 msgid "Pending retries" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:273 +#: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:290 msgid "Recovered Renewal Revenue" msgstr "" @@ -1548,75 +1651,78 @@ msgstr "" msgid "Cannot create subscription: %s." msgstr "" -#: includes/class-wc-subscription.php:413 +#: includes/class-wc-subscription.php:415 msgid "Unable to change subscription status to \"%s\"." msgstr "" -#: includes/class-wc-subscription.php:515 +#: includes/class-wc-subscription.php:527 msgid "Unable to change subscription status to \"%s\". Exception: %s" msgstr "" -#: includes/class-wc-subscription.php:537 +#: includes/class-wc-subscription.php:557 #. translators: 1: old subscription status 2: new subscription status msgid "Status changed from %1$s to %2$s." msgstr "" -#: includes/class-wc-subscription.php:549 +#: includes/class-wc-subscription.php:571 #. translators: %s: new order status msgid "Status set to %s." msgstr "" -#: includes/class-wc-subscription.php:1100 +#: includes/class-wc-subscription.php:585 +msgid "Error during subscription status transition." +msgstr "" + +#: includes/class-wc-subscription.php:1132 #: includes/class-wc-subscriptions-manager.php:2279 -#: includes/wcs-formatting-functions.php:228 #. translators: placeholder is human time diff (e.g. "3 weeks") msgid "In %s" msgstr "" -#: includes/class-wc-subscription.php:1103 +#: includes/class-wc-subscription.php:1135 #: includes/wcs-formatting-functions.php:231 #. translators: placeholder is human time diff (e.g. "3 weeks") msgid "%s ago" msgstr "" -#: includes/class-wc-subscription.php:1110 +#: includes/class-wc-subscription.php:1142 msgid "Not yet ended" msgstr "" -#: includes/class-wc-subscription.php:1113 +#: includes/class-wc-subscription.php:1145 msgid "Not cancelled" msgstr "" -#: includes/class-wc-subscription.php:1228 +#: includes/class-wc-subscription.php:1260 msgid "The creation date of a subscription can not be deleted, only updated." msgstr "" -#: includes/class-wc-subscription.php:1231 +#: includes/class-wc-subscription.php:1263 msgid "The start date of a subscription can not be deleted, only updated." msgstr "" -#: includes/class-wc-subscription.php:1235 +#: includes/class-wc-subscription.php:1267 msgid "The %s date of a subscription can not be deleted. You must delete the order." msgstr "" -#: includes/class-wc-subscription.php:1243 -#: includes/class-wc-subscription.php:2302 +#: includes/class-wc-subscription.php:1275 +#: includes/class-wc-subscription.php:2360 msgid "Subscription #%d: " msgstr "" -#: includes/class-wc-subscription.php:1650 +#: includes/class-wc-subscription.php:1682 msgid "Payment status marked complete." msgstr "" -#: includes/class-wc-subscription.php:1678 +#: includes/class-wc-subscription.php:1710 msgid "Payment failed." msgstr "" -#: includes/class-wc-subscription.php:1683 +#: includes/class-wc-subscription.php:1715 msgid "Subscription Cancelled: maximum number of failed payments reached." msgstr "" -#: includes/class-wc-subscription.php:1793 +#: includes/class-wc-subscription.php:1825 msgid "" "The \"all\" value for $order_type parameter is deprecated. It was a " "misnomer, as it did not return resubscribe orders. It was also inconsistent " @@ -1626,50 +1732,45 @@ msgid "" "resubscribe." msgstr "" -#: includes/class-wc-subscription.php:1895 -#: includes/class-wcs-change-payment-method-admin.php:155 -msgid "Manual Renewal" -msgstr "" - -#: includes/class-wc-subscription.php:1974 wcs-functions.php:794 +#: includes/class-wc-subscription.php:2022 wcs-functions.php:794 msgid "Payment method meta must be an array." msgstr "" -#: includes/class-wc-subscription.php:2200 +#: includes/class-wc-subscription.php:2258 msgid "Invalid format. First parameter needs to be an array." msgstr "" -#: includes/class-wc-subscription.php:2204 +#: includes/class-wc-subscription.php:2262 msgid "Invalid data. First parameter was empty when passed to update_dates()." msgstr "" -#: includes/class-wc-subscription.php:2211 +#: includes/class-wc-subscription.php:2269 msgid "" "Invalid data. First parameter has a date that is not in the registered date " "types." msgstr "" -#: includes/class-wc-subscription.php:2275 +#: includes/class-wc-subscription.php:2333 msgid "The %s date must occur after the cancellation date." msgstr "" -#: includes/class-wc-subscription.php:2280 +#: includes/class-wc-subscription.php:2338 msgid "The %s date must occur after the last payment date." msgstr "" -#: includes/class-wc-subscription.php:2284 +#: includes/class-wc-subscription.php:2342 msgid "The %s date must occur after the next payment date." msgstr "" -#: includes/class-wc-subscription.php:2289 +#: includes/class-wc-subscription.php:2347 msgid "The %s date must occur after the trial end date." msgstr "" -#: includes/class-wc-subscription.php:2293 +#: includes/class-wc-subscription.php:2351 msgid "The %s date must occur after the start date." msgstr "" -#: includes/class-wc-subscription.php:2322 +#: includes/class-wc-subscription.php:2380 #: includes/class-wc-subscriptions-checkout.php:325 #: includes/wcs-order-functions.php:305 msgid "Backordered" @@ -1691,93 +1792,109 @@ msgstr "" msgid "Update the %1$s used for %2$sall%3$s of my active subscriptions" msgstr "" -#: includes/class-wc-subscriptions-cart.php:912 +#: includes/class-wc-subscriptions-cart.php:932 msgid "Please enter a valid postcode/ZIP." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1083 +#: includes/class-wc-subscriptions-cart.php:1090 msgid "" "That subscription product can not be added to your cart as it already " "contains a subscription renewal." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1171 +#: includes/class-wc-subscriptions-cart.php:1178 msgid "Invalid recurring shipping method." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1994 +#: includes/class-wc-subscriptions-cart.php:2016 msgid "now" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:175 +#: includes/class-wc-subscriptions-change-payment-gateway.php:179 #: templates/emails/plain/email-order-details.php:19 #. translators: placeholder is the subscription order number wrapped in #. tags msgid "Subscription Number: %s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:181 +#: includes/class-wc-subscriptions-change-payment-gateway.php:185 #. translators: placeholder is the subscription's next payment date (either #. human readable or normal date) wrapped in tags msgid "Next Payment Date: %s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:187 +#: includes/class-wc-subscriptions-change-payment-gateway.php:191 #. translators: placeholder is the formatted total to be paid for the #. subscription wrapped in tags msgid "Total: %s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:194 +#: includes/class-wc-subscriptions-change-payment-gateway.php:198 #. translators: placeholder is the display name of the payment method msgid "Payment Method: %s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:206 +#: includes/class-wc-subscriptions-change-payment-gateway.php:210 msgid "" "Sorry, this subscription change payment method request is invalid and " "cannot be processed." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:238 +#: includes/class-wc-subscriptions-change-payment-gateway.php:242 msgid "There was an error with your request. Please try again." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:242 -#: templates/myaccount/view-subscription.php:20 +#: includes/class-wc-subscriptions-change-payment-gateway.php:246 +#: includes/class-wcs-template-loader.php:27 msgid "Invalid Subscription." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:246 +#: includes/class-wc-subscriptions-change-payment-gateway.php:250 #: includes/class-wcs-cart-resubscribe.php:78 #: includes/class-wcs-cart-resubscribe.php:129 #: includes/class-wcs-user-change-status-handler.php:103 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:94 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:95 msgid "That doesn't appear to be one of your subscriptions." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:250 +#: includes/class-wc-subscriptions-change-payment-gateway.php:254 +#: includes/class-wcs-query.php:252 msgid "The payment method can not be changed for that subscription." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:256 +#: includes/class-wc-subscriptions-change-payment-gateway.php:260 #. translators: placeholder is next payment's date msgid " Next payment is due %s." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:262 +#: includes/class-wc-subscriptions-change-payment-gateway.php:266 #. translators: placeholder is either empty or "Next payment is due..." msgid "Choose a new payment method.%s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:291 +#: includes/class-wc-subscriptions-change-payment-gateway.php:295 msgid "Invalid order." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:393 +#: includes/class-wc-subscriptions-change-payment-gateway.php:381 msgid "Payment method updated." msgstr "" +#: includes/class-wc-subscriptions-change-payment-gateway.php:381 +msgid "Payment method added." +msgstr "" + +#: includes/class-wc-subscriptions-change-payment-gateway.php:421 +#: includes/class-wc-subscriptions-change-payment-gateway.php:423 +msgid "Payment method updated for all your current subscriptions." +msgstr "" + +#: includes/class-wc-subscriptions-change-payment-gateway.php:832 +msgid "" +"Please log in to your account below to choose a new payment method for your " +"subscription." +msgstr "" + #: includes/class-wc-subscriptions-checkout.php:185 #: includes/class-wc-subscriptions-checkout.php:356 #. translators: placeholder is an internal error number @@ -1914,7 +2031,7 @@ msgid "Error: Unable to create renewal order with note \"%s\"" msgstr "" #: includes/class-wc-subscriptions-manager.php:168 -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:209 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:211 msgid "Subscription doesn't exist in scheduled action: %d" msgstr "" @@ -2135,7 +2252,7 @@ msgstr "" msgid "%1$s and a %2$s sign-up fee" msgstr "" -#: includes/class-wc-subscriptions-product.php:947 +#: includes/class-wc-subscriptions-product.php:951 msgid "" "This variation can not be removed because it is associated with active " "subscriptions. To remove this variation, please cancel and delete the " @@ -2471,7 +2588,7 @@ msgid "There was an error with your request to resubscribe. Please try again." msgstr "" #: includes/class-wcs-cart-resubscribe.php:74 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:90 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:91 msgid "That subscription does not exist. Has it been deleted?" msgstr "" @@ -2495,13 +2612,11 @@ msgid "Please choose a valid payment gateway to change to." msgstr "" #: includes/class-wcs-failed-scheduled-action-manager.php:134 -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:178 -msgid "Ignore this error (not recommended)" +msgid "Ignore this error" msgstr "" #: includes/class-wcs-failed-scheduled-action-manager.php:139 -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:183 -msgid "Open a ticket" +msgid "Learn more" msgstr "" #: includes/class-wcs-limiter.php:45 @@ -2527,14 +2642,28 @@ msgstr "" msgid "Limit to one of any status" msgstr "" -#: includes/class-wcs-my-account-payment-methods.php:82 +#: includes/class-wcs-my-account-auto-renew-toggle.php:132 +msgid "Auto Renewal Toggle" +msgstr "" + +#: includes/class-wcs-my-account-auto-renew-toggle.php:133 +msgid "Display the auto renewal toggle" +msgstr "" + +#: includes/class-wcs-my-account-auto-renew-toggle.php:134 +msgid "" +"Allow customers to turn on and off automatic renewals from their View " +"Subscription page." +msgstr "" + +#: includes/class-wcs-my-account-payment-methods.php:80 msgid "" "The deleted payment method was used for automatic subscription payments, we " "couldn't find an alternative token payment method token to change your " "subscriptions to." msgstr "" -#: includes/class-wcs-my-account-payment-methods.php:106 +#: includes/class-wcs-my-account-payment-methods.php:102 #. translators: $1: the token/credit card label, 2$-3$: opening and closing #. strong and link tags msgid "" @@ -2545,42 +2674,58 @@ msgid "" "Subscriptions%3$s page." msgstr "" -#: includes/class-wcs-my-account-payment-methods.php:194 +#: includes/class-wcs-my-account-payment-methods.php:121 msgid "%s ending in %s" msgstr "" -#: includes/class-wcs-my-account-payment-methods.php:286 +#: includes/class-wcs-my-account-payment-methods.php:153 msgid "" "Would you like to update your subscriptions to use this new payment method " "- %1$s?%2$sYes%4$s | %3$sNo%4$s" msgstr "" +#: includes/class-wcs-permalink-manager.php:91 +#. translators: 1$-2$: opening and closing tags. +msgid "" +"Error saving Subscriptions endpoints: %1$sSubscriptions%2$s, %1$sView " +"subscription%2$s and %1$sSubscription payment method%2$s cannot be the " +"same. The changes have been reverted." +msgstr "" + #: includes/class-wcs-post-meta-cache-manager.php:198 msgid "" "Invalid update type: %s. Post update types supported are \"add\" or " "\"delete\". Updates are done on post meta directly." msgstr "" -#: includes/class-wcs-query.php:106 +#: includes/class-wcs-query.php:113 msgid "Subscriptions (page %d)" msgstr "" -#: includes/class-wcs-query.php:127 +#: includes/class-wcs-query.php:140 msgid "My Subscription" msgstr "" -#: includes/class-wcs-query.php:249 +#: includes/class-wcs-query.php:297 msgid "Endpoint for the My Account → Subscriptions page" msgstr "" -#: includes/class-wcs-query.php:257 +#: includes/class-wcs-query.php:305 msgid "View subscription" msgstr "" -#: includes/class-wcs-query.php:258 +#: includes/class-wcs-query.php:306 msgid "Endpoint for the My Account → View Subscription page" msgstr "" +#: includes/class-wcs-query.php:314 +msgid "Subscription payment method" +msgstr "" + +#: includes/class-wcs-query.php:315 +msgid "Endpoint for the My Account → Change Subscription Payment Method page" +msgstr "" + #: includes/class-wcs-remove-item.php:108 msgid "Your request to undo your previous action was unsuccessful." msgstr "" @@ -2610,7 +2755,7 @@ msgid "" "not support removing an item." msgstr "" -#: includes/class-wcs-retry-manager.php:316 +#: includes/class-wcs-retry-manager.php:347 msgid "" "Payment retry attempted on renewal order with multiple related " "subscriptions with no payment method in common." @@ -2626,6 +2771,10 @@ msgstr "" msgid "staging" msgstr "" +#: includes/class-wcs-template-loader.php:27 +msgid "My Account" +msgstr "" + #: includes/class-wcs-user-change-status-handler.php:60 msgid "" "You can not reactivate that subscription until paying to renew it. Please " @@ -2713,42 +2862,41 @@ msgid "" "related order queries are run." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:70 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:71 msgid "Renew Now" msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:98 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:99 msgid "" "You can not renew this subscription early. Please contact us if you need " "assistance." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:104 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:105 msgid "Complete checkout to renew now." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:226 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:247 #. translators: placeholder contains a link to the order's edit screen. msgid "Customer successfully renewed early with order %s." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:229 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:250 #. translators: placeholder contains a link to the order's edit screen. msgid "" "Failed to update subscription dates after customer renewed early with order " "%s." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:317 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:338 msgid "Order %s created to record early renewal." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:372 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:393 msgid "Cancel" msgstr "" #: includes/early-renewal/class-wcs-early-renewal-manager.php:44 -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:98 msgid "Early Renewal" msgstr "" @@ -2776,41 +2924,37 @@ msgstr "" msgid "Subscription Cancelled" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:127 -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:192 -#: includes/emails/class-wcs-email-expired-subscription.php:125 -#: includes/emails/class-wcs-email-on-hold-subscription.php:125 +#: includes/emails/class-wcs-email-cancelled-subscription.php:147 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:214 +#: includes/emails/class-wcs-email-expired-subscription.php:145 +#: includes/emails/class-wcs-email-on-hold-subscription.php:145 msgid "Enable this email notification" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:134 -#: includes/emails/class-wcs-email-expired-subscription.php:132 -#: includes/emails/class-wcs-email-on-hold-subscription.php:132 +#: includes/emails/class-wcs-email-cancelled-subscription.php:154 +#: includes/emails/class-wcs-email-expired-subscription.php:152 +#: includes/emails/class-wcs-email-on-hold-subscription.php:152 #. translators: placeholder is admin email -msgid "" -"Enter recipients (comma separated) for this email. Defaults to " -"%s." +msgid "Enter recipients (comma separated) for this email. Defaults to %s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:141 -#: includes/emails/class-wcs-email-expired-subscription.php:139 -#: includes/emails/class-wcs-email-on-hold-subscription.php:139 +#: includes/emails/class-wcs-email-cancelled-subscription.php:161 +#: includes/emails/class-wcs-email-expired-subscription.php:159 +#: includes/emails/class-wcs-email-on-hold-subscription.php:159 msgid "" "This controls the email subject line. Leave blank to use the default " -"subject: %s." +"subject: %s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:148 -#: includes/emails/class-wcs-email-expired-subscription.php:146 -#: includes/emails/class-wcs-email-on-hold-subscription.php:146 +#: includes/emails/class-wcs-email-cancelled-subscription.php:168 msgid "" "This controls the main heading contained within the email notification. " -"Leave blank to use the default heading: %s." +"Leave blank to use the default heading: %s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:155 -#: includes/emails/class-wcs-email-expired-subscription.php:153 -#: includes/emails/class-wcs-email-on-hold-subscription.php:153 +#: includes/emails/class-wcs-email-cancelled-subscription.php:175 +#: includes/emails/class-wcs-email-expired-subscription.php:173 +#: includes/emails/class-wcs-email-on-hold-subscription.php:173 msgid "Choose which format of email to send." msgstr "" @@ -2907,11 +3051,18 @@ msgstr "" msgid "Subscription Expired" msgstr "" -#: includes/emails/class-wcs-email-expired-subscription.php:58 -#: includes/emails/class-wcs-email-on-hold-subscription.php:58 +#: includes/emails/class-wcs-email-expired-subscription.php:78 +#: includes/emails/class-wcs-email-on-hold-subscription.php:78 msgid "Subscription argument passed in is not an object." msgstr "" +#: includes/emails/class-wcs-email-expired-subscription.php:166 +#: includes/emails/class-wcs-email-on-hold-subscription.php:166 +msgid "" +"This controls the main heading contained within the email notification. " +"Leave blank to use the default heading: %s." +msgstr "" + #: includes/emails/class-wcs-email-new-renewal-order.php:22 msgid "New Renewal Order" msgstr "" @@ -2999,56 +3150,69 @@ msgstr "" msgid "Your {blogname} renewal order receipt from {order_date}" msgstr "" -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:131 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:133 msgid "" "Sorry, it seems there are no available payment methods which support " "subscriptions. Please contact us if you require assistance or wish to make " "alternate arrangements." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:199 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:268 +msgid "Supported features:" +msgstr "" + +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:271 +msgid "Subscription features:" +msgstr "" + +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:275 +msgid "Change payment features:" +msgstr "" + +#: includes/gateways/paypal/class-wcs-paypal.php:213 msgid "Unable to find order for PayPal billing agreement." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:256 +#: includes/gateways/paypal/class-wcs-paypal.php:275 msgid "An error occurred, please try again or try an alternate form of payment." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:360 +#: includes/gateways/paypal/class-wcs-paypal.php:379 #. translators: placeholders are PayPal API error code and PayPal API error #. message msgid "PayPal API error: (%d) %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:365 +#: includes/gateways/paypal/class-wcs-paypal.php:384 #. translators: placeholder is PayPal transaction status message msgid "PayPal Transaction Held: %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:377 +#: includes/gateways/paypal/class-wcs-paypal.php:396 #. translators: placeholder is PayPal transaction status message msgid "PayPal payment declined: %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:381 +#: includes/gateways/paypal/class-wcs-paypal.php:400 msgid "PayPal payment approved (ID: %s)" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:434 +#: includes/gateways/paypal/class-wcs-paypal.php:453 msgid "" "Are you sure you want to change the payment method from PayPal standard?\n" "\n" "This will suspend the subscription at PayPal." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:59 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:63 +#. translators: $1 and $2 are opening and closing strong tags, respectively. msgid "" -"It is strongly recommended you do not change the Receiver Email " -"address if you have active subscriptions with PayPal. Doing so can " -"break existing subscriptions." +"It is %sstrongly recommended you do not change the Receiver Email address%s " +"if you have active subscriptions with PayPal. Doing so can break existing " +"subscriptions." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:105 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:109 #. translators: placeholders are opening and closing link tags. 1$-2$: to docs #. on woocommerce, 3$-4$ to gateway settings on the site msgid "" @@ -3057,7 +3221,7 @@ msgid "" "Subscriptions." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:116 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:120 #. translators: placeholders are opening and closing strong and link tags. #. 1$-2$: strong tags, 3$-8$ link to docs on woocommerce msgid "" @@ -3067,14 +3231,14 @@ msgid "" "%5$sCheck PayPal Account%6$s %3$sLearn more %7$s" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:132 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:136 #. translators: placeholders are opening and closing strong tags. msgid "" "%1$sPayPal Reference Transactions are enabled on your account%2$s. All " "subscription management features are now enabled. Happy selling!" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:143 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:147 #. translators: placeholders are link opening and closing tags. 1$-2$: to #. gateway settings, 3$-4$: support docs on woocommerce.com msgid "" @@ -3082,7 +3246,7 @@ msgid "" "Please update your %1$sAPI credentials%2$s. %3$sLearn more%4$s." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:156 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:160 #. translators: placeholders are opening and closing link tags. 1$-2$: docs on #. woocommerce, 3$-4$: dismiss link msgid "" @@ -3090,10 +3254,32 @@ msgid "" "subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:265 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:182 +msgid "Ignore this error (not recommended)" +msgstr "" + +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:187 +msgid "Open a ticket" +msgstr "" + +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:269 msgid "PayPal Subscription ID:" msgstr "" +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:296 +msgid "Enable PayPal Standard for Subscriptions" +msgstr "" + +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:304 +#. translators: Placeholders are the opening and closing link tags. +msgid "" +"Before enabling PayPal Standard for Subscriptions, please note, when using " +"PayPal Standard, customers are locked into using PayPal Standard for the " +"life of their subscription, and PayPal Standard has a number of " +"limitations. Please read the guide on %swhy we don't recommend PayPal " +"Standard%s for Subscriptions before choosing to enable this option." +msgstr "" + #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:274 msgid "Total Discount" msgstr "" @@ -3116,32 +3302,32 @@ msgstr "" msgid "Billing agreement cancelled at PayPal." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:278 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:279 msgid "IPN subscription sign up completed." msgstr "" #: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:332 -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:415 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:416 msgid "IPN subscription payment completed." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:377 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:378 msgid "IPN subscription failing payment method changed." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:467 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:468 msgid "IPN subscription suspended." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:490 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:491 msgid "IPN subscription cancelled." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:506 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:507 msgid "IPN subscription payment failure." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:644 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:645 msgid "Invalid PayPal IPN Payload: unable to find matching subscription." msgstr "" @@ -3435,13 +3621,13 @@ msgstr "" msgid "Customers with a subscription are excluded from this setting." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:330 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:332 #. translators: placeholder is a list of version numbers (e.g. "1.3 & 1.4 & #. 1.5") msgid "Database updated to version %s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:347 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:349 #. translators: 1$: number of action scheduler hooks upgraded, 2$: #. "{execution_time}", will be replaced on front end with actual time msgid "" @@ -3449,28 +3635,29 @@ msgid "" "seconds)." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:359 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:361 #. translators: 1$: number of subscriptions upgraded, 2$: "{execution_time}", #. will be replaced on front end with actual time it took msgid "Migrated %1$s subscriptions to the new structure (in %2$s seconds)." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:372 -#. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag +#: includes/upgrades/class-wc-subscriptions-upgrader.php:374 +#. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, +#. 4$: break tag msgid "" -"Unable to upgrade subscriptions.
    Error: %1$s
    Please refresh the " -"page and try again. If problem persists, %2$scontact support%3$s." +"Unable to upgrade subscriptions.%4$sError: %1$s%4$sPlease refresh the page " +"and try again. If problem persists, %2$scontact support%3$s." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:623 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:625 msgid "Welcome to WooCommerce Subscriptions 2.1" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:623 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:625 msgid "About WooCommerce Subscriptions" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:804 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:806 msgid "" "%1$sWarning!%2$s It appears that you have downgraded %1$sWooCommerce " "Subscriptions%2$s from %3$s to %4$s. Downgrading the plugin in this way may " @@ -3478,7 +3665,7 @@ msgid "" "ticket%6$s for further assistance. %7$sLearn more »%8$s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:892 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:875 msgid "" "%1$sWarning!%2$s We discovered an issue in %1$sWooCommerce Subscriptions " "2.3.0 - 2.3.2%2$s that may cause your subscription renewal order and " @@ -3497,54 +3684,53 @@ msgid "Subscription end date in the past" msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:85 -msgid "New Subscription Coupon Features" +msgid "New options to allow customers to sign up without a credit card" msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:86 msgid "" -"Want to offer customers coupons which apply for 6 months? You can now " -"define the number of cycles discounts would be applied." +"Allow customers to access free trial and other $0 subscription products " +"without needing to enter their credit card details on sign up." msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:89 -msgid "New Signup Pricing Options for Synchronized Subscriptions" +msgid "Improved subscription payment method information" msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:90 msgid "" -"Charge the full recurring price at the time of sign up for synchronized " -"subscriptions. Your customers can now receive their products straight away." +"Customers can now see more information about what payment method will be " +"used for future payments." msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:93 -msgid "Link Parent Orders to Subscriptions" +msgid "Auto-renewal toggle" msgstr "" -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:95 -#. translators: placeholders are opening and closing tags linking to -#. documentation. +#: includes/upgrades/class-wcs-upgrade-notice-manager.php:94 msgid "" -"For subscriptions with no parent order, shop managers can now choose a " -"parent order via the Edit Subscription screen. This makes it possible to " -"set a parent order on %smanually created subscriptions%s. The order can " -"also be sent to customers to act as an invoice that needs to be paid to " -"activate the subscription." +"Enabled via a setting, this new feature will allow your customers to turn " +"on and off automatic payments from the %sMy Account > View Subscription%s " +"pages." msgstr "" -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:99 +#: includes/upgrades/class-wcs-upgrade-notice-manager.php:97 +msgid "Update all subscription payment methods" +msgstr "" + +#: includes/upgrades/class-wcs-upgrade-notice-manager.php:98 msgid "" -"Customers can now renew their subscriptions before the scheduled next " -"payment date. Why not use this to email your customers a coupon a month " -"before their annual subscription renewals to get access to that revenue " -"sooner?" +"Customers will now have the option to update all their subscriptions when " +"they are changing one of their subscription's payment methods - provided " +"the payment gateway supports it." msgstr "" -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:104 +#: includes/upgrades/class-wcs-upgrade-notice-manager.php:103 #. translators: placeholder is Subscription version string ('2.3') msgid "Welcome to Subscriptions %s" msgstr "" -#: includes/upgrades/class-wcs-upgrade-notice-manager.php:111 +#: includes/upgrades/class-wcs-upgrade-notice-manager.php:110 msgid "Learn More" msgstr "" @@ -4137,7 +4323,7 @@ msgstr "" #: includes/upgrades/templates/wcs-upgrade.php:43 msgid "" "Customers and other non-administrative users can browse and purchase from " -"your store without interuption while the update is in progress." +"your store without interruption while the update is in progress." msgstr "" #: includes/upgrades/templates/wcs-upgrade.php:49 @@ -4160,7 +4346,7 @@ msgstr "" msgid "" "Remember, although the update process may take a while, customers and other " "non-administrative users can browse and purchase from your store without " -"interuption while the update is in progress." +"interruption while the update is in progress." msgstr "" #: includes/upgrades/templates/wcs-upgrade.php:61 @@ -4271,7 +4457,7 @@ msgid "%1$s %2$s then %3$s on the %4$s day of every %5$s month" msgstr "" #: includes/wcs-formatting-functions.php:161 -#. translators: 1$: initial amount, 2$: intial description (e.g. "up front"), +#. translators: 1$: initial amount, 2$: initial description (e.g. "up front"), #. 3$: recurring amount, 4$: month of year (e.g. "March"), 5$: day of the month #. (e.g. "23rd") msgid "%1$s %2$s then %3$s on %4$s %5$s each year" @@ -4304,6 +4490,11 @@ msgstr "" msgid "%1$s free trial then %2$s" msgstr "" +#: includes/wcs-formatting-functions.php:228 +#. translators: placeholder is human time diff (e.g. "3 weeks") +msgid "in %s" +msgstr "" + #: includes/wcs-helper-functions.php:38 #. translators: date placeholder for input, javascript format msgid "YYYY-MM-DD" @@ -4421,38 +4612,30 @@ msgstr "" msgid "Shipping Tax:" msgstr "" -#: templates/admin/html-failed-scheduled-action-notice.php:16 -#. translators: $1 and $2 are opening and closing link tags, respectively. +#: templates/admin/html-failed-scheduled-action-notice.php:15 msgid "" "An error has occurred while processing a recent subscription related event. " -"Please %1$sopen a new ticket at WooCommerce Support%2$s immediately to get " -"this resolved." +"For steps on how to fix the affected subscription and to learn more about " +"the possible causes of this error, please read our guide %1$shere%2$s." msgid_plural "" "An error has occurred while processing recent subscription related events. " -"Please %1$sopen a new ticket at WooCommerce Support%2$s immediately to get " -"this resolved." +"For steps on how to fix the affected subscriptions and to learn more about " +"the possible causes of this error, please read our guide %1$shere%2$s." msgstr[0] "" msgstr[1] "" -#: templates/admin/html-failed-scheduled-action-notice.php:29 -#. translators: $1 and $2 are opening and closing link tags, respectively. -msgid "" -"To resolve this error as quickly as possible, please create a %1$stemporary " -"administrator account%2$s with the user email support@prospress.com." -msgstr "" - -#: templates/admin/html-failed-scheduled-action-notice.php:34 +#: templates/admin/html-failed-scheduled-action-notice.php:25 msgid "Affected event:" msgid_plural "Affected events:" msgstr[0] "" msgstr[1] "" -#: templates/admin/html-failed-scheduled-action-notice.php:41 +#: templates/admin/html-failed-scheduled-action-notice.php:32 #. translators: $1 the log file name $2 and $3 are opening and closing link #. tags, respectively. msgid "" -"To see further details, view the %1$s log file from the %2$sWooCommerce " -"logs screen.%2$s" +"To see further details about these errors, view the %1$s log file from the " +"%2$sWooCommerce logs screen.%2$s" msgstr "" #: templates/admin/html-variation-price.php:31 @@ -4481,13 +4664,18 @@ msgid "" "or contact us if you need any help." msgstr "" -#: templates/checkout/form-change-payment-method.php:82 +#: templates/checkout/form-change-payment-method.php:92 msgid "" "Sorry, it seems no payment gateways support changing the recurring payment " "method. Please contact us if you require assistance or to make alternate " "arrangements." msgstr "" +#: templates/checkout/form-change-payment-method.php:101 +#. translators: $1: opening tag, $2: closing tag +msgid "Update the payment method used for %1$sall%2$s of my current subscriptions" +msgstr "" + #: templates/checkout/recurring-totals.php:19 msgid "Recurring Totals" msgstr "" @@ -4627,19 +4815,19 @@ msgstr "" msgid "ID" msgstr "" -#: templates/myaccount/my-subscriptions.php:70 +#: templates/myaccount/my-subscriptions.php:65 msgid "Previous" msgstr "" -#: templates/myaccount/my-subscriptions.php:74 +#: templates/myaccount/my-subscriptions.php:69 msgid "Next" msgstr "" -#: templates/myaccount/my-subscriptions.php:81 +#: templates/myaccount/my-subscriptions.php:76 msgid "You have reached the end of subscriptions. Go to the %sfirst page%s." msgstr "" -#: templates/myaccount/my-subscriptions.php:84 +#: templates/myaccount/my-subscriptions.php:79 #. translators: placeholders are opening and closing link tags to take to the #. shop page msgid "" @@ -4651,7 +4839,7 @@ msgstr "" msgid "Order" msgstr "" -#: templates/myaccount/related-orders.php:51 +#: templates/myaccount/related-orders.php:56 #. translators: $1: formatted order total for the order, $2: number of items #. bought msgid "%1$s for %2$d item" @@ -4663,11 +4851,27 @@ msgstr[1] "" msgid "Related Subscriptions" msgstr "" -#: templates/myaccount/subscription-details.php:43 +#: templates/myaccount/subscription-details.php:40 +msgid "Auto Renew" +msgstr "" + +#: templates/myaccount/subscription-details.php:45 +msgid "Enable auto renew" +msgstr "" + +#: templates/myaccount/subscription-details.php:50 +msgid "Disable auto renew" +msgstr "" + +#: templates/myaccount/subscription-details.php:60 +msgid "Payment" +msgstr "" + +#: templates/myaccount/subscription-details.php:70 msgid "Actions" msgstr "" -#: templates/myaccount/subscription-details.php:56 +#: templates/myaccount/subscription-details.php:83 msgid "Subscription Updates" msgstr "" @@ -4679,10 +4883,6 @@ msgstr "" msgid "Are you sure you want remove this item from your subscription?" msgstr "" -#: templates/myaccount/view-subscription.php:20 -msgid "My Account" -msgstr "" - #: templates/single-product/add-to-cart/subscription.php:45 #: templates/single-product/add-to-cart/variable-subscription.php:31 msgid "You have an active subscription to this product already." @@ -4716,71 +4916,81 @@ msgstr "" msgid "Date type can not be an empty string." msgstr "" -#: woocommerce-subscriptions.php:268 +#: woocommerce-subscriptions.php:269 msgid "This is where subscriptions are stored." msgstr "" -#: woocommerce-subscriptions.php:313 +#: woocommerce-subscriptions.php:314 msgid "No Subscriptions found" msgstr "" -#: woocommerce-subscriptions.php:315 +#: woocommerce-subscriptions.php:316 msgid "" "Subscriptions will appear here for you to view and manage once purchased by " "a customer." msgstr "" -#: woocommerce-subscriptions.php:317 +#: woocommerce-subscriptions.php:318 #. translators: placeholders are opening and closing link tags msgid "%sLearn more about managing subscriptions »%s" msgstr "" -#: woocommerce-subscriptions.php:319 +#: woocommerce-subscriptions.php:320 #. translators: placeholders are opening and closing link tags msgid "%sAdd a subscription product »%s" msgstr "" -#: woocommerce-subscriptions.php:475 +#: woocommerce-subscriptions.php:377 +msgid "" +"To enable automatic renewals for this subscription, you will first need to " +"add a payment method." +msgstr "" + +#: woocommerce-subscriptions.php:377 +msgid "Would you like to add a payment method now?" +msgstr "" + +#: woocommerce-subscriptions.php:493 msgid "" "A subscription renewal has been removed from your cart. Multiple " "subscriptions can not be purchased at the same time." msgstr "" -#: woocommerce-subscriptions.php:481 +#: woocommerce-subscriptions.php:499 msgid "" "A subscription has been removed from your cart. Due to payment gateway " "restrictions, different subscription products can not be purchased at the " "same time." msgstr "" -#: woocommerce-subscriptions.php:487 +#: woocommerce-subscriptions.php:505 msgid "" "A subscription has been removed from your cart. Products and subscriptions " "can not be purchased at the same time." msgstr "" -#: woocommerce-subscriptions.php:629 woocommerce-subscriptions.php:646 +#: woocommerce-subscriptions.php:647 woocommerce-subscriptions.php:664 #. translators: placeholder is a number, this is for the teens #. translators: placeholder is a number, numbers ending in 4-9, 0 msgid "%sth" msgstr "" -#: woocommerce-subscriptions.php:634 +#: woocommerce-subscriptions.php:652 #. translators: placeholder is a number, numbers ending in 1 msgid "%sst" msgstr "" -#: woocommerce-subscriptions.php:638 +#: woocommerce-subscriptions.php:656 #. translators: placeholder is a number, numbers ending in 2 msgid "%snd" msgstr "" -#: woocommerce-subscriptions.php:642 +#: woocommerce-subscriptions.php:660 #. translators: placeholder is a number, numbers ending in 3 msgid "%srd" msgstr "" -#: woocommerce-subscriptions.php:672 +#: woocommerce-subscriptions.php:690 #. translators: 1$-2$: opening and closing tags, 3$-4$: link tags, #. takes to woocommerce plugin on wp.org, 5$-6$: opening and closing link tags, #. leads to plugins.php in admin @@ -4790,7 +5000,7 @@ msgid "" "%5$sinstall & activate WooCommerce »%6$s" msgstr "" -#: woocommerce-subscriptions.php:675 +#: woocommerce-subscriptions.php:693 #. translators: 1$-2$: opening and closing tags, 3$: minimum supported #. WooCommerce version, 4$-5$: opening and closing link tags, leads to plugin #. admin @@ -4800,11 +5010,11 @@ msgid "" "WooCommerce to version %3$s or newer »%5$s" msgstr "" -#: woocommerce-subscriptions.php:706 +#: woocommerce-subscriptions.php:724 msgid "Variable Subscription" msgstr "" -#: woocommerce-subscriptions.php:801 +#: woocommerce-subscriptions.php:820 msgid "" "%1$sWarning!%2$s We can see the %1$sWooCommerce Subscriptions Early " "Renewal%2$s plugin is active. Version %3$s of %1$sWooCommerce " @@ -4813,11 +5023,11 @@ msgid "" "avoid any conflicts." msgstr "" -#: woocommerce-subscriptions.php:804 +#: woocommerce-subscriptions.php:823 msgid "Installed Plugins" msgstr "" -#: woocommerce-subscriptions.php:873 +#: woocommerce-subscriptions.php:892 #. translators: 1$-2$: opening and closing tags. 3$-4$: opening and #. closing link tags for learn more. Leads to duplicate site article on docs. #. 5$-6$: Opening and closing link to production URL. 7$: Production URL . @@ -4829,19 +5039,19 @@ msgid "" "the site's URL. %3$sLearn more »%4$s." msgstr "" -#: woocommerce-subscriptions.php:882 +#: woocommerce-subscriptions.php:901 msgid "Quit nagging me (but don't enable automatic payments)" msgstr "" -#: woocommerce-subscriptions.php:887 +#: woocommerce-subscriptions.php:906 msgid "Enable automatic payments" msgstr "" -#: woocommerce-subscriptions.php:1093 +#: woocommerce-subscriptions.php:1112 msgid "Support" msgstr "" -#: woocommerce-subscriptions.php:1176 +#: woocommerce-subscriptions.php:1195 #. translators: placeholders are opening and closing tags. Leads to docs on #. version 2 msgid "" @@ -4852,14 +5062,14 @@ msgid "" "2.0 »%s" msgstr "" -#: woocommerce-subscriptions.php:1191 +#: woocommerce-subscriptions.php:1210 msgid "" "Warning! You are running version %s of WooCommerce Subscriptions plugin " "code but your database has been upgraded to Subscriptions version 2.0. This " "will cause major problems on your store." msgstr "" -#: woocommerce-subscriptions.php:1192 +#: woocommerce-subscriptions.php:1211 msgid "" "Please upgrade the WooCommerce Subscriptions plugin to version 2.0 or newer " "immediately. If you need assistance, after upgrading to Subscriptions v2.0, " @@ -4884,7 +5094,7 @@ msgstr "" msgid "https://prospress.com/" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:206 +#: includes/admin/class-wc-subscriptions-admin.php:280 #. translators: placeholder is trial period validation message if passed an #. invalid value (e.g. "Trial period can not exceed 4 weeks") msgctxt "Trial period field tooltip on Edit Product administration screen" @@ -4894,12 +5104,12 @@ msgid "" "subscription. %s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:219 +#: includes/admin/class-wc-subscriptions-admin.php:293 msgctxt "example price" msgid "e.g. 5.90" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:253 +#: includes/admin/class-wc-subscriptions-admin.php:328 #: templates/admin/deprecated/html-variation-price.php:31 #: templates/admin/deprecated/html-variation-price.php:86 #: templates/admin/html-variation-price.php:21 @@ -4908,7 +5118,7 @@ msgctxt "example price" msgid "e.g. 9.90" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:790 +#: includes/admin/class-wc-subscriptions-admin.php:872 #. translators: placeholders are for HTML tags. They are 1$: "

    ", 2$: #. "

    ", 3$: "

    ", 4$: "", 5$: "", 6$: "", 7$: "", 8$: #. "

    " @@ -4919,7 +5129,7 @@ msgid "" "%6$sVariable subscription%7$s.%8$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:792 +#: includes/admin/class-wc-subscriptions-admin.php:874 #. translators: placeholders are for HTML tags. They are 1$: "

    ", 2$: #. "

    ", 3$: "

    ", 4$: "

    " msgctxt "used in admin pointer script params in javascript as price pointer content" @@ -4929,17 +5139,17 @@ msgid "" "sign-up fee and free trial.%4$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1124 +#: includes/admin/class-wc-subscriptions-admin.php:1206 msgctxt "option section heading" msgid "Renewals" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1156 +#: includes/admin/class-wc-subscriptions-admin.php:1238 msgctxt "options section heading" msgid "Miscellaneous" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1164 +#: includes/admin/class-wc-subscriptions-admin.php:1246 msgctxt "there's a number immediately in front of this text" msgid "suspensions per billing period." msgstr "" @@ -4949,25 +5159,25 @@ msgctxt "there's a number immediately in front of this text" msgid "days prior to Renewal Day" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1445 +#: includes/admin/class-wc-subscriptions-admin.php:1573 #: includes/admin/class-wcs-admin-system-status.php:93 msgctxt "label that indicates whether debugging is turned on for the plugin" msgid "WCS_DEBUG" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1451 +#: includes/admin/class-wc-subscriptions-admin.php:1579 #: includes/admin/class-wcs-admin-system-status.php:107 msgctxt "Live or Staging, Label on WooCommerce -> System Status page" msgid "Subscriptions Mode" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1452 +#: includes/admin/class-wc-subscriptions-admin.php:1580 #: includes/admin/class-wcs-admin-system-status.php:109 msgctxt "refers to staging site" msgid "Staging" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1452 +#: includes/admin/class-wc-subscriptions-admin.php:1580 #: includes/admin/class-wcs-admin-system-status.php:109 msgctxt "refers to live site" msgid "Live" @@ -4997,7 +5207,7 @@ msgstr "" #: includes/admin/class-wcs-admin-post-types.php:473 #: includes/class-wc-subscriptions-manager.php:1829 #: includes/wcs-user-functions.php:349 -#: templates/myaccount/related-orders.php:73 +#: templates/myaccount/related-orders.php:78 msgctxt "an action on a subscription" msgid "Cancel" msgstr "" @@ -5024,13 +5234,13 @@ msgctxt "Subscription title on admin table. (e.g.: #211 for John Doe)" msgid "%1$s#%2$s%3$s for %4$s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:889 +#: includes/admin/class-wcs-admin-post-types.php:898 #. translators: placeholder is previous post title msgctxt "used in post updated messages" msgid "Subscription restored to revision from %s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:894 +#: includes/admin/class-wcs-admin-post-types.php:903 msgctxt "used in \"Subscription scheduled for \"" msgid "M j, Y @ G:i" msgstr "" @@ -5139,25 +5349,25 @@ msgctxt "" msgid "Gateway ID: [%s]" msgstr "" -#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:344 +#: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:359 msgctxt "subscription note after linking to a parent order" msgid "Subscription linked to parent order %s via admin." msgstr "" #: includes/admin/meta-boxes/views/html-related-orders-row.php:19 #: includes/class-wc-subscriptions-renewal-order.php:157 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:219 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:316 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:240 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:337 #: templates/myaccount/my-subscriptions.php:38 -#: templates/myaccount/related-orders.php:39 +#: templates/myaccount/related-orders.php:44 #: templates/myaccount/related-subscriptions.php:32 msgctxt "hash before order number" msgid "#%s" msgstr "" #: includes/class-wc-subscriptions-addresses.php:206 -#: includes/class-wc-subscriptions-change-payment-gateway.php:616 -#: includes/class-wcs-query.php:102 +#: includes/class-wc-subscriptions-change-payment-gateway.php:776 +#: includes/class-wcs-query.php:109 msgctxt "hash before order number" msgid "Subscription #%s" msgstr "" @@ -5178,7 +5388,7 @@ msgctxt "table heading" msgid "Total" msgstr "" -#: includes/class-wcs-retry-manager.php:119 +#: includes/class-wcs-retry-manager.php:120 msgctxt "table heading" msgid "Renewal Payment Retry" msgstr "" @@ -5191,13 +5401,13 @@ msgid "Last Order Date" msgstr "" #: templates/emails/subscription-info.php:19 -#: templates/myaccount/subscription-details.php:22 wcs-functions.php:300 +#: templates/myaccount/subscription-details.php:21 wcs-functions.php:300 msgctxt "table heading" msgid "Start Date" msgstr "" #: templates/emails/subscription-info.php:20 -#: templates/myaccount/subscription-details.php:28 wcs-functions.php:305 +#: templates/myaccount/subscription-details.php:27 wcs-functions.php:305 msgctxt "table heading" msgid "End Date" msgstr "" @@ -5225,7 +5435,7 @@ msgctxt "table heading" msgid "Cancelled Date" msgstr "" -#: includes/admin/reports/class-wcs-report-cache-manager.php:326 +#: includes/admin/reports/class-wcs-report-cache-manager.php:329 msgctxt "Whether the Report Cache has been enabled" msgid "Report Cache Enabled" msgstr "" @@ -5256,12 +5466,12 @@ msgctxt "API response confirming order note deleted from a subscription" msgid "Permanently deleted subscription note" msgstr "" -#: includes/class-wc-subscription.php:1118 +#: includes/class-wc-subscription.php:1150 msgctxt "original denotes there is no date to display" msgid "-" msgstr "" -#: includes/class-wc-subscription.php:2238 +#: includes/class-wc-subscription.php:2296 #. translators: placeholder is date type (e.g. "end", "next_payment"...) msgctxt "appears in an error message if date is wrong format" msgid "Invalid %s date. The date must be of the format: \"Y-m-d H:i:s\"." @@ -5272,24 +5482,35 @@ msgctxt "change billing or shipping address" msgid "Change %s address" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:315 +#: includes/class-wc-subscriptions-change-payment-gateway.php:318 msgctxt "label on button, imperative" msgid "Change Payment" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:441 +#: includes/class-wc-subscriptions-change-payment-gateway.php:320 +msgctxt "label on button, imperative" +msgid "Add Payment" +msgstr "" + +#: includes/class-wc-subscriptions-change-payment-gateway.php:570 msgctxt "%1$s: old payment title, %2$s: new payment title" msgid "" "Payment method changed from \"%1$s\" to \"%2$s\" by the subscriber from " "their account page." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:587 -#: includes/class-wc-subscriptions-change-payment-gateway.php:621 +#: includes/class-wc-subscriptions-change-payment-gateway.php:745 +#: includes/class-wc-subscriptions-change-payment-gateway.php:782 msgctxt "the page title of the change payment method form" msgid "Change Payment Method" msgstr "" +#: includes/class-wc-subscriptions-change-payment-gateway.php:747 +#: includes/class-wc-subscriptions-change-payment-gateway.php:787 +msgctxt "the page title of the add payment method form" +msgid "Add Payment Method" +msgstr "" + #: includes/class-wc-subscriptions-manager.php:87 #: includes/class-wc-subscriptions-manager.php:1893 #: includes/class-wc-subscriptions-manager.php:1911 @@ -5297,13 +5518,13 @@ msgctxt "used in order note as reason for why subscription status changed" msgid "Subscription renewal payment due:" msgstr "" -#: includes/class-wcs-retry-manager.php:300 +#: includes/class-wcs-retry-manager.php:331 msgctxt "used in order note as reason for why subscription status changed" msgid "Subscription renewal payment retry:" msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:149 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:176 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:150 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:197 msgctxt "used in order note as reason for why subscription status changed" msgid "Customer requested to renew early:" msgstr "" @@ -5529,14 +5750,14 @@ msgctxt "" msgid "All linked subscription items were" msgstr "" -#: includes/class-wcs-my-account-payment-methods.php:101 +#: includes/class-wcs-my-account-payment-methods.php:97 msgctxt "used in subscription note" msgid "" "Payment method meta updated after customer deleted a token from their My " "Account page. Payment meta changed from %1$s to %2$s" msgstr "" -#: includes/class-wcs-my-account-payment-methods.php:328 +#: includes/class-wcs-my-account-payment-methods.php:193 msgctxt "used in subscription note" msgid "" "Payment method meta updated after customer changed their default token and " @@ -5560,25 +5781,30 @@ msgctxt "used in order note" msgid "Customer removed \"%1$s\" (Product ID: #%2$d) via the My Account page." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:425 -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:434 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:426 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:435 #. translators: placeholder is payment status (e.g. "completed") msgctxt "used in order note" msgid "IPN subscription payment %s." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:438 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:439 #. translators: placeholder is payment status (e.g. "completed") msgctxt "used in order note" msgid "IPN subscription payment %s for reason: %s." msgstr "" -#: includes/class-wcs-retry-manager.php:229 +#: includes/class-wcs-retry-manager.php:230 msgctxt "used in order note as reason for why status changed" msgid "Retry rule applied:" msgstr "" -#: includes/class-wcs-retry-manager.php:296 +#: includes/class-wcs-retry-manager.php:268 +msgctxt "used in order note as reason for why status changed" +msgid "Retry rule reapplied:" +msgstr "" + +#: includes/class-wcs-retry-manager.php:327 msgctxt "used in order note as reason for why order status changed" msgid "Subscription renewal payment retry:" msgstr "" @@ -5620,61 +5846,61 @@ msgctxt "default email subject for cancelled emails sent to the admin" msgid "[%s] Subscription Cancelled" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:125 -#: includes/emails/class-wcs-email-customer-renewal-invoice.php:190 -#: includes/emails/class-wcs-email-expired-subscription.php:123 -#: includes/emails/class-wcs-email-on-hold-subscription.php:123 +#: includes/emails/class-wcs-email-cancelled-subscription.php:145 +#: includes/emails/class-wcs-email-customer-renewal-invoice.php:212 +#: includes/emails/class-wcs-email-expired-subscription.php:143 +#: includes/emails/class-wcs-email-on-hold-subscription.php:143 msgctxt "an email notification" msgid "Enable/Disable" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:131 -#: includes/emails/class-wcs-email-expired-subscription.php:129 -#: includes/emails/class-wcs-email-on-hold-subscription.php:129 +#: includes/emails/class-wcs-email-cancelled-subscription.php:151 +#: includes/emails/class-wcs-email-expired-subscription.php:149 +#: includes/emails/class-wcs-email-on-hold-subscription.php:149 msgctxt "of an email" msgid "Recipient(s)" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:139 -#: includes/emails/class-wcs-email-expired-subscription.php:137 -#: includes/emails/class-wcs-email-on-hold-subscription.php:137 +#: includes/emails/class-wcs-email-cancelled-subscription.php:159 +#: includes/emails/class-wcs-email-expired-subscription.php:157 +#: includes/emails/class-wcs-email-on-hold-subscription.php:157 msgctxt "of an email" msgid "Subject" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:146 -#: includes/emails/class-wcs-email-expired-subscription.php:144 -#: includes/emails/class-wcs-email-on-hold-subscription.php:144 +#: includes/emails/class-wcs-email-cancelled-subscription.php:166 +#: includes/emails/class-wcs-email-expired-subscription.php:164 +#: includes/emails/class-wcs-email-on-hold-subscription.php:164 msgctxt "" "Name the setting that controls the main heading contained within the email " "notification" msgid "Email Heading" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:153 -#: includes/emails/class-wcs-email-expired-subscription.php:151 -#: includes/emails/class-wcs-email-on-hold-subscription.php:151 +#: includes/emails/class-wcs-email-cancelled-subscription.php:173 +#: includes/emails/class-wcs-email-expired-subscription.php:171 +#: includes/emails/class-wcs-email-on-hold-subscription.php:171 msgctxt "text, html or multipart" msgid "Email type" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:159 -#: includes/emails/class-wcs-email-expired-subscription.php:157 -#: includes/emails/class-wcs-email-on-hold-subscription.php:157 +#: includes/emails/class-wcs-email-cancelled-subscription.php:179 +#: includes/emails/class-wcs-email-expired-subscription.php:177 +#: includes/emails/class-wcs-email-on-hold-subscription.php:177 msgctxt "email type" msgid "Plain text" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:160 -#: includes/emails/class-wcs-email-expired-subscription.php:158 -#: includes/emails/class-wcs-email-on-hold-subscription.php:158 +#: includes/emails/class-wcs-email-cancelled-subscription.php:180 +#: includes/emails/class-wcs-email-expired-subscription.php:178 +#: includes/emails/class-wcs-email-on-hold-subscription.php:178 msgctxt "email type" msgid "HTML" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:161 -#: includes/emails/class-wcs-email-expired-subscription.php:159 -#: includes/emails/class-wcs-email-on-hold-subscription.php:159 +#: includes/emails/class-wcs-email-cancelled-subscription.php:181 +#: includes/emails/class-wcs-email-expired-subscription.php:179 +#: includes/emails/class-wcs-email-on-hold-subscription.php:179 msgctxt "email type" msgid "Multipart" msgstr "" @@ -5719,7 +5945,7 @@ msgctxt "default email subject for suspended emails sent to the admin" msgid "[%s] Subscription Suspended" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:336 +#: includes/gateways/paypal/class-wcs-paypal.php:355 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:208 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:315 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:326 @@ -5734,7 +5960,7 @@ msgctxt "" msgid "#" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:532 +#: includes/gateways/paypal/class-wcs-paypal.php:619 msgctxt "" "used in User Agent data sent to PayPal to help identify where a payment " "came from" @@ -5770,7 +5996,7 @@ msgctxt "no information about something" msgid "N/A" msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:274 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:275 msgctxt "" "when it is a payment change, and there is a subscr_signup message, this " "will be a confirmation message that PayPal accepted it being the new " @@ -5801,21 +6027,21 @@ msgctxt "Admin menu name" msgid "Renewal Payment Retries" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:338 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:340 #. translators: placeholder is number of upgraded subscriptions msgctxt "used in the subscriptions upgrader" msgid "Marked %s subscription products as \"sold individually\"." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:362 -#: includes/upgrades/class-wc-subscriptions-upgrader.php:408 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:364 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:410 #. translators: placeholder is "{time_left}", will be replaced on front end #. with actual time msgctxt "Message that gets sent to front end." msgid "Estimated time left (minutes:seconds): %s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:387 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:389 #. translators: placeholder is the number of subscriptions repaired msgctxt "Repair message that gets sent to front end." msgid "" @@ -5823,7 +6049,7 @@ msgid "" "customer notes." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:393 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:395 #. translators: placeholder is number of subscriptions that were checked and #. did not need repairs. There's a space at the beginning! msgctxt "Repair message that gets sent to front end." @@ -5832,14 +6058,14 @@ msgid_plural "%d other subscriptions were checked and did not need any repairs." msgstr[0] "" msgstr[1] "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:397 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:399 #. translators: placeholder is "{execution_time}", which will be replaced on #. front end with actual time msgctxt "Repair message that gets sent to front end." msgid "(in %s seconds)" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:400 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:402 #. translators: $1: "Repaired x subs with incorrect dates...", $2: "X others #. were checked and no repair needed", $3: "(in X seconds)". Ordering for RTL #. languages. @@ -5847,21 +6073,22 @@ msgctxt "The assembled repair message that gets sent to front end." msgid "%1$s%2$s %3$s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:419 -#. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag +#: includes/upgrades/class-wc-subscriptions-upgrader.php:421 +#. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, +#. 4$: break tag msgctxt "Error message that gets sent to front end when upgrading Subscriptions" msgid "" -"Unable to repair subscriptions.
    Error: %1$s
    Please refresh the page " +"Unable to repair subscriptions.%4$sError: %1$s%4$sPlease refresh the page " "and try again. If problem persists, %2$scontact support%3$s." msgstr "" #: includes/upgrades/class-wcs-upgrade-notice-manager.php:80 msgctxt "plugin version number used in admin notice" -msgid "2.3" +msgid "2.5" msgstr "" #: includes/upgrades/templates/wcs-about-2-0.php:36 -#: woocommerce-subscriptions.php:1092 +#: woocommerce-subscriptions.php:1111 msgctxt "short for documents" msgid "Docs" msgstr "" @@ -5918,6 +6145,13 @@ msgctxt "initial payment on a subscription" msgid "up front" msgstr "" +#: includes/wcs-formatting-functions.php:236 +#. translators: placeholder is a localized date and time (e.g. "February 1, +#. 2018 10:20 PM") +msgctxt "wcs_get_human_time_diff" +msgid "%s" +msgstr "" + #: includes/wcs-order-functions.php:146 msgctxt "" "In wcs_copy_order_meta error message. Refers to origin and target order " @@ -6124,11 +6358,16 @@ msgctxt "table headings in notification email" msgid "Date Suspended" msgstr "" -#: templates/checkout/form-change-payment-method.php:55 +#: templates/checkout/form-change-payment-method.php:57 msgctxt "text on button on checkout page" msgid "Change Payment Method" msgstr "" +#: templates/checkout/form-change-payment-method.php:59 +msgctxt "text on button on checkout page" +msgid "Add Payment Method" +msgstr "" + #: templates/emails/admin-new-renewal-order.php:16 #: templates/emails/plain/admin-new-renewal-order.php:16 #. translators: $1: customer's billing first name and last name @@ -6279,41 +6518,41 @@ msgctxt "subscription number in email table. (eg: #106)" msgid "#%s" msgstr "" -#: templates/myaccount/my-subscriptions.php:55 -#: templates/myaccount/related-orders.php:48 +#: templates/myaccount/my-subscriptions.php:50 +#: templates/myaccount/related-orders.php:53 #: templates/myaccount/related-subscriptions.php:41 msgctxt "Used in data attribute. Escaped" msgid "Total" msgstr "" -#: templates/myaccount/my-subscriptions.php:59 -#: templates/myaccount/related-orders.php:79 +#: templates/myaccount/my-subscriptions.php:54 +#: templates/myaccount/related-orders.php:84 #: templates/myaccount/related-subscriptions.php:45 msgctxt "view a subscription" msgid "View" msgstr "" -#: templates/myaccount/related-orders.php:60 +#: templates/myaccount/related-orders.php:65 msgctxt "pay for a subscription" msgid "Pay" msgstr "" -#: templates/myaccount/subscription-details.php:26 +#: templates/myaccount/subscription-details.php:25 msgctxt "admin subscription table header" msgid "Last Order Date" msgstr "" -#: templates/myaccount/subscription-details.php:27 +#: templates/myaccount/subscription-details.php:26 msgctxt "admin subscription table header" msgid "Next Payment Date" msgstr "" -#: templates/myaccount/subscription-details.php:29 +#: templates/myaccount/subscription-details.php:28 msgctxt "admin subscription table header" msgid "Trial End Date" msgstr "" -#: templates/myaccount/subscription-details.php:62 +#: templates/myaccount/subscription-details.php:89 msgctxt "date on subscription updates list. Will be localized" msgid "l jS \\o\\f F Y, h:ia" msgstr "" @@ -6346,68 +6585,68 @@ msgctxt "The post title for the new subscription" msgid "Subscription – %s" msgstr "" -#: woocommerce-subscriptions.php:255 +#: woocommerce-subscriptions.php:256 msgctxt "custom post type setting" msgid "Add Subscription" msgstr "" -#: woocommerce-subscriptions.php:256 +#: woocommerce-subscriptions.php:257 msgctxt "custom post type setting" msgid "Add New Subscription" msgstr "" -#: woocommerce-subscriptions.php:257 +#: woocommerce-subscriptions.php:258 msgctxt "custom post type setting" msgid "Edit" msgstr "" -#: woocommerce-subscriptions.php:258 +#: woocommerce-subscriptions.php:259 msgctxt "custom post type setting" msgid "Edit Subscription" msgstr "" -#: woocommerce-subscriptions.php:259 +#: woocommerce-subscriptions.php:260 msgctxt "custom post type setting" msgid "New Subscription" msgstr "" -#: woocommerce-subscriptions.php:260 woocommerce-subscriptions.php:261 +#: woocommerce-subscriptions.php:261 woocommerce-subscriptions.php:262 msgctxt "custom post type setting" msgid "View Subscription" msgstr "" -#: woocommerce-subscriptions.php:264 +#: woocommerce-subscriptions.php:265 msgctxt "custom post type setting" msgid "No Subscriptions found in trash" msgstr "" -#: woocommerce-subscriptions.php:265 +#: woocommerce-subscriptions.php:266 msgctxt "custom post type setting" msgid "Parent Subscriptions" msgstr "" -#: woocommerce-subscriptions.php:333 +#: woocommerce-subscriptions.php:334 msgctxt "post status label including post count" msgid "Active (%s)" msgid_plural "Active (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:334 +#: woocommerce-subscriptions.php:335 msgctxt "post status label including post count" msgid "Switched (%s)" msgid_plural "Switched (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:335 +#: woocommerce-subscriptions.php:336 msgctxt "post status label including post count" msgid "Expired (%s)" msgid_plural "Expired (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:336 +#: woocommerce-subscriptions.php:337 msgctxt "post status label including post count" msgid "Pending Cancellation (%s)" msgid_plural "Pending Cancellation (%s)" diff --git a/templates/admin/html-failed-scheduled-action-notice.php b/templates/admin/html-failed-scheduled-action-notice.php index d98d6bc..a4252d9 100755 --- a/templates/admin/html-failed-scheduled-action-notice.php +++ b/templates/admin/html-failed-scheduled-action-notice.php @@ -2,7 +2,7 @@ /** * The template for displaying an admin notice to report failed Subscriptions related scheduled actions. * - * @version 2.4.0 + * @version 2.5.0 * @var array $failed_scheduled_actions * @var string $affected_subscription_events */ @@ -11,25 +11,16 @@ if ( ! defined( 'ABSPATH' ) ) { } ?>

    ', + '', '' - );?> -

    -

    ', - '' - );?> + )?>

    failed-scheduled-actions', '', '' diff --git a/templates/admin/html-variation-price.php b/templates/admin/html-variation-price.php index 02f8268..932c338 100755 --- a/templates/admin/html-variation-price.php +++ b/templates/admin/html-variation-price.php @@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) {