Updates to 7.8.0

This commit is contained in:
WooCommerce
2025-08-20 10:17:55 +00:00
parent 8d2f68f6e7
commit 7041a483f0
156 changed files with 23425 additions and 5392 deletions

View File

@@ -85,7 +85,8 @@ a.close-subscriptions-search {
float: right;
}
#woocommerce-product-data .variable_subscription_pricing ._subscription_length_field .wc_input_subscription_length + .select2,
#woocommerce-product-data .variable_subscription_sync .subscription_sync_week_month .wc_input_subscription_payment_sync + .select2 {
#woocommerce-product-data .variable_subscription_sync .subscription_sync_week_month .wc_input_subscription_payment_sync + .select2,
#woocommerce-product-data .variable_subscription_gifting ._subscription_gifting_field .wc_input_subscription_gifting + .select2 {
width: 100% !important;
}
/* Simple Subscription Product Sync Settings */
@@ -98,6 +99,31 @@ a.close-subscriptions-search {
#general_product_data .subscription_sync_annual .select2-container:not( :last-child ) {
width: 50% !important;
}
/* Simple Subscription Product Gifting Settings */
._subscription_gifting_field .select2-container {
width: 50% !important;
}
p.show_if_variable-subscription._subscription_gifting_field {
margin-top: 0;
margin-bottom: 0;
}
p._subscription_gifting_field.overriding-store-settings {
margin-bottom: 0;
}
p._subscription_gifting_field_description.form-field {
padding-top: 0 !important;
margin-top: 0;
}
p._subscription_gifting_field_description.form-field .description {
display: block;
clear: both;
margin-left: 0;
}
.variable_subscription_gifting p._subscription_gifting_field_description {
margin-bottom: 0;
margin-top: 6px;
}
@media only screen and ( max-width: 1280px ) {
.woocommerce_options_panel ._subscription_price_fields .wrap,
@@ -107,6 +133,9 @@ a.close-subscriptions-search {
.wrap {
width: 80%;
}
._subscription_gifting_field .select2-container {
width: 80% !important;
}
}
.woocommerce_options_panel ._subscription_price_fields .wrap input,
.woocommerce_options_panel ._subscription_price_fields .wrap select {
@@ -359,6 +388,7 @@ a.close-subscriptions-search {
/* Variation Pricing Fields with WooCommerce 3.0+ */
.wc-metaboxes-wrapper .variable_subscription_trial label,
.wc-metaboxes-wrapper .variable_subscription_pricing label,
.wc-metaboxes-wrapper .variable_subscription_gifting label,
.wc-metaboxes-wrapper .variable_subscription_sync label {
display: block;
}
@@ -947,3 +977,23 @@ table.form-table input#woocommerce_subscriptions_customer_notifications_offset {
.show_if_subscription .select2-selection, .show_if_variable-subscription .select2-selection {
font-size: 14px;
}
.wc-settings-row-enable-gifting.checked .titledesc {
padding-bottom: 14px;
}
.wc-settings-row-enable-gifting.checked .forminp-checkbox {
padding-bottom: 0;
margin-bottom: 0;
}
.wc-settings-row-gifting-radios td {
padding-top: 0;
}
.wc-settings-row-gifting-radios th {
padding-top: 0;
}
.wc-settings-row-gifting-radios li:nth-child(2) label {
margin-bottom: 0 !important;
}
.wc-settings-row-gifting-radios p {
margin: 2px 0 5px;
color: #646970;
}

View File

@@ -0,0 +1,30 @@
.wc-shortcode-components-validation-error {
display: none;
}
#shortcode-validate-error-invalid-gifting-recipient {
font-size: 0.75em;
display: flex;
align-items: center;
margin-top: -12px;
margin-bottom: 20px;
}
#shortcode-validate-error-invalid-gifting-recipient svg {
fill: var(--wc-red, #cc1818);
}
#shortcode-validate-error-invalid-gifting-recipient span {
color: var(--wc-red, #cc1818);
font-size: 12px;
font-weight: 500;
font-style: normal;
line-height: 16px;
}
.woocommerce .woocommerce_subscriptions_gifting_recipient_email .input-text.recipient_email.wcsg-email-error {
border-color: var(--wc-red, #cc1818);
color: var(--wc-red, #cc1818);
}

View File

@@ -0,0 +1,26 @@
<svg width="141" height="115" viewBox="0 0 141 115" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M104.686 23.5272H42.6647C38.3831 23.5272 34.9121 26.9982 34.9121 31.2798V86.8401C34.9121 91.1218 38.3831 94.5927 42.6647 94.5927H104.686C108.967 94.5927 112.438 91.1218 112.438 86.8401V31.2798C112.438 26.9982 108.967 23.5272 104.686 23.5272Z" fill="#D1C1FF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M104.686 23.5272H42.6647C38.3827 23.5272 34.9121 26.9978 34.9121 31.2798V42.9087H112.438V31.2798C112.438 26.9978 108.968 23.5272 104.686 23.5272Z" fill="#A77EFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M112.434 61.4844V86.8405C112.434 91.1226 108.963 94.5932 104.681 94.5932H79.3252C97.611 94.5932 112.434 79.7702 112.434 61.4844Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M73.6715 34.8336C76.3476 34.8336 78.5169 32.6642 78.5169 29.9882C78.5169 27.3122 76.3476 25.1428 73.6715 25.1428C70.9955 25.1428 68.8262 27.3122 68.8262 29.9882C68.8262 32.6642 70.9955 34.8336 73.6715 34.8336Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M54.2917 34.8336C56.9677 34.8336 59.137 32.6642 59.137 29.9882C59.137 27.3122 56.9677 25.1428 54.2917 25.1428C51.6156 25.1428 49.4463 27.3122 49.4463 29.9882C49.4463 32.6642 51.6156 34.8336 54.2917 34.8336Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M76.9023 29.0193V19.6516C76.9023 18.2244 75.7454 17.0674 74.3181 17.0674H73.026C71.5988 17.0674 70.4418 18.2244 70.4418 19.6516V29.0193C70.4418 30.4465 71.5988 31.6035 73.026 31.6035H74.3181C75.7454 31.6035 76.9023 30.4465 76.9023 29.0193Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M57.5215 29.0193V19.6516C57.5215 18.2244 56.3645 17.0674 54.9373 17.0674H53.6452C52.218 17.0674 51.061 18.2244 51.061 19.6516V29.0193C51.061 30.4465 52.218 31.6035 53.6452 31.6035H54.9373C56.3645 31.6035 57.5215 30.4465 57.5215 29.0193Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M61.6236 82.6491C59.458 82.6491 57.6956 80.8866 57.6956 78.7211V63.6707C57.6956 63.5285 57.5457 63.5234 57.5431 63.5234C57.5121 63.5234 57.4863 63.5337 57.4553 63.557L56.4733 64.3503C55.8867 64.8258 55.1864 65.0765 54.4499 65.0765C52.659 65.0765 51.2041 63.6242 51.2041 61.8385C51.2041 60.8126 51.6977 59.8383 52.5246 59.231L58.7164 54.6828C59.458 54.1376 60.3367 53.8507 61.2566 53.8507C63.6238 53.8507 65.549 55.7759 65.549 58.1431V78.7236C65.549 80.8892 63.7866 82.6516 61.621 82.6516L61.6236 82.6491Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-miterlimit="10"/>
<path d="M72.0717 82.6493C70.0587 82.6493 68.4229 81.0109 68.4229 79.0004C68.4229 77.7109 69.1154 76.504 70.2266 75.8502L80.4213 69.8807C82.3078 68.7979 83.7911 67.8547 84.8377 67.0743C85.8274 66.3378 86.5278 65.6581 86.9231 65.056C87.2849 64.5056 87.4581 63.9371 87.4581 63.3194C87.4581 62.6656 87.2875 62.1049 86.9387 61.6087C86.5872 61.11 86.0393 60.7094 85.308 60.4174C84.5198 60.1021 83.4887 59.9419 82.2432 59.9419C80.7727 59.9419 79.5659 60.1512 78.6589 60.5621C77.7906 60.9549 77.1394 61.4795 76.7233 62.1204C76.5786 62.3426 76.452 62.5752 76.346 62.8129C75.7155 64.2368 74.3303 65.1568 72.8212 65.1568C71.312 65.1568 70.1388 64.4229 69.4488 63.1902C68.7691 61.9757 68.795 60.5466 69.5186 59.3656C69.7356 59.0142 69.976 58.6705 70.237 58.3423C71.5265 56.7297 73.2527 55.4686 75.3666 54.5978C77.4469 53.7398 79.8347 53.3057 82.4576 53.3057C85.0806 53.3057 87.1945 53.6985 89.0965 54.4711C91.0398 55.2619 92.5825 56.3938 93.686 57.8358C94.8127 59.3114 95.3838 61.0453 95.3838 62.9912C95.3838 64.4952 94.9884 65.9114 94.2106 67.2009C93.4586 68.4465 92.2621 69.6973 90.6573 70.9248C89.1016 72.1135 87.0213 73.4056 84.4707 74.7649L82.8995 75.6306C82.8659 75.6487 82.7988 75.6848 82.8272 75.796C82.8556 75.9071 82.9306 75.9071 82.9693 75.9071H92.7712C94.6292 75.9071 96.141 77.4188 96.141 79.2769C96.141 81.1349 94.6292 82.6467 92.7712 82.6467H72.0717V82.6493Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-miterlimit="10"/>
<path d="M33.3676 95.9851L2.50293 46.8893L21.0797 35.2107L51.9444 84.3065C52.6064 85.3596 52.998 86.5619 53.0802 87.8047L55.4775 106.262C55.754 108.384 53.4144 109.855 51.6221 108.686L36.0278 98.5249C34.9456 97.9108 34.0297 97.0381 33.3676 95.9851Z" fill="#B999FF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.0813 35.211L27.7547 45.8263C29.1375 48.0258 26.0982 52.4217 20.9689 55.6463C15.8396 58.8709 10.5607 59.7043 9.17795 57.5049L2.5045 46.8896C1.12176 44.6901 4.16103 40.2942 9.29035 37.0695C14.4197 33.8449 19.6985 33.0115 21.0813 35.211Z" fill="#E1D7FF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.0786 35.2102L18.4248 36.8785L51.7921 89.9551L51.9433 84.306L21.0786 35.2102Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27.7526 45.8261L31.0893 51.1337C32.472 53.3332 29.4328 57.7291 24.3034 60.9538C19.1741 64.1784 13.8952 65.0118 12.5125 62.8123L9.17578 57.5046C10.5585 59.7041 15.8374 58.8707 20.9667 55.6461C26.096 52.4215 29.1353 48.0256 27.7526 45.8261Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.2954 45.031C19.4253 41.8061 22.4634 37.4095 21.0813 35.211C19.6992 33.0125 14.4202 33.8446 9.29035 37.0695C4.16051 40.2945 1.12238 44.6911 2.5045 46.8896C3.88662 49.0881 9.1656 48.256 14.2954 45.031Z" fill="#D1C1FF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M51.6229 108.686C53.4153 109.855 55.7549 108.384 55.4784 106.262L53.0811 87.8044C52.9967 86.5629 51.7731 84.0323 51.1111 82.9793L50.0662 87.3389C49.5551 88.9902 48.5434 91.0184 46.833 90.7666L45.6279 90.5883C43.9182 90.333 42.2792 91.3634 41.766 93.0161L41.4043 94.1794C40.8933 95.8307 38.6256 95.8611 36.9139 95.6073L32.5322 94.6592C33.1942 95.7122 34.9422 97.9131 36.0265 98.5259L51.6208 108.687L51.6229 108.686Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M54.3923 97.999L55.4678 106.269C55.7443 108.39 53.4046 109.861 51.6123 108.692L44.6782 104.174C46.3741 104.186 48.3947 103.588 50.3012 102.39C52.2077 101.191 53.6832 99.5615 54.3923 97.999Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M136.455 23.6709C134.348 23.6709 132.471 22.3832 131.667 20.4353C131.658 20.4135 131.649 20.3936 131.64 20.3718C130.827 18.4203 131.237 16.1768 132.732 14.6823L135.238 12.1758L128.825 5.76269L126.319 8.26918C124.824 9.76365 122.581 10.1717 120.629 9.3592C120.607 9.35013 120.587 9.34106 120.566 9.33199C118.618 8.52854 117.33 6.65139 117.33 4.54391V1H108.262V4.54572C108.262 6.6532 106.974 8.53035 105.026 9.33381C105.004 9.34288 104.984 9.35194 104.963 9.36101C103.011 10.1735 100.768 9.76365 99.2731 8.271L96.7666 5.76451L90.3535 12.1776L92.86 14.6841C94.3545 16.1786 94.7625 18.4239 93.9518 20.3736C93.9428 20.3954 93.9337 20.4153 93.9246 20.4371C93.1212 22.385 91.244 23.6727 89.1365 23.6727H85.5908V32.741H89.1365C91.244 32.741 93.1212 34.0287 93.9246 35.9766C93.9337 35.9984 93.9428 36.0183 93.9518 36.0401C94.7643 37.9916 94.3545 40.2351 92.86 41.7296L90.3535 44.236L96.7666 50.6492L99.2731 48.1427C100.768 46.6482 103.011 46.2401 104.963 47.0527C104.984 47.0617 105.004 47.0708 105.026 47.0799C106.974 47.8833 108.262 49.7605 108.262 51.868V55.4137H117.33V51.868C117.33 49.7605 118.618 47.8833 120.566 47.0799C120.587 47.0708 120.607 47.0617 120.629 47.0527C122.581 46.2401 124.824 46.65 126.319 48.1427L128.825 50.6492L135.238 44.236L132.732 41.7296C131.237 40.2351 130.829 37.9898 131.64 36.0401C131.649 36.0183 131.658 35.9984 131.667 35.9766C132.471 34.0287 134.348 32.741 136.455 32.741H140.001V23.6727H136.455V23.6709ZM112.796 37.2734C107.788 37.2734 103.728 33.2126 103.728 28.205C103.728 23.1975 107.788 19.1367 112.796 19.1367C117.803 19.1367 121.864 23.1975 121.864 28.205C121.864 33.2126 117.803 37.2734 112.796 37.2734Z" fill="#F2EDFF" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<g style="mix-blend-mode:multiply">
<path d="M121.832 28.2051C121.832 33.2127 117.771 37.2735 112.764 37.2735C110.259 37.2735 107.992 36.2578 106.351 34.6183L93.5264 47.4427L96.7329 50.6493L99.2394 48.1428C100.734 46.6483 102.977 46.2403 104.929 47.0528C104.951 47.0619 104.971 47.0709 104.992 47.08C106.94 47.8834 108.228 49.7606 108.228 51.8681V55.4138H117.296V51.8681C117.296 49.7606 118.584 47.8834 120.532 47.08C120.554 47.0709 120.574 47.0619 120.595 47.0528C122.547 46.2403 124.79 46.6502 126.285 48.1428L128.791 50.6493L135.204 44.2362L132.698 41.7297C131.204 40.2352 130.795 37.9899 131.606 36.0402C131.615 36.0184 131.624 35.9985 131.633 35.9767C132.437 34.0288 134.314 32.7411 136.421 32.7411H139.967V23.6728H136.421C134.314 23.6728 132.437 22.3851 131.633 20.4372C131.624 20.4154 131.615 20.3955 131.606 20.3737C130.794 18.4222 131.204 16.1787 132.698 14.6842L135.204 12.1778L131.998 8.97119L119.173 21.7956C120.815 23.437 121.829 25.7041 121.829 28.2088L121.832 28.2051Z" fill="#B999FF"/>
</g>
<path d="M112.798 14.6027C105.286 14.6027 99.1953 20.693 99.1953 28.2052C99.1953 35.7174 105.286 41.8077 112.798 41.8077C120.31 41.8077 126.4 35.7174 126.4 28.2052C126.4 20.693 120.31 14.6027 112.798 14.6027ZM112.798 37.2735C107.79 37.2735 103.729 33.2127 103.729 28.2052C103.729 23.1976 107.79 19.1368 112.798 19.1368C117.805 19.1368 121.866 23.1976 121.866 28.2052C121.866 33.2127 117.805 37.2735 112.798 37.2735Z" fill="#F2EDFF"/>
<path d="M112.796 37.2735C117.804 37.2735 121.864 33.2135 121.864 28.2052C121.864 23.1969 117.804 19.1368 112.796 19.1368C107.788 19.1368 103.728 23.1969 103.728 28.2052C103.728 33.2135 107.788 37.2735 112.796 37.2735Z" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M112.798 14.6027C120.31 14.6027 126.4 20.693 126.4 28.2052C126.4 35.7174 120.31 41.8077 112.798 41.8077C105.286 41.8077 99.1953 35.7174 99.1953 28.2052" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M121.893 18.0848C124.105 20.5043 125.458 23.7253 125.458 27.2638C125.458 34.776 119.368 40.8663 111.856 40.8663C108.317 40.8663 105.096 39.5151 102.677 37.3006C105.163 40.0193 108.74 41.7242 112.715 41.7242C120.228 41.7242 126.318 35.6339 126.318 28.1217C126.318 24.1461 124.613 20.5714 121.894 18.083L121.893 18.0848Z" fill="#2C045D" stroke="#2C045D" stroke-width="1.62161" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -406,6 +406,27 @@ jQuery( function ( $ ) {
$( this ).addClass( 'wcs_moved' );
} );
},
moveSubscriptionGiftingFields: function () {
$( '#variable_product_options .variable_subscription_gifting' )
.not( '.wcs_gifting_moved' )
.each( function () {
var $trialSignUpRow = $( this ).siblings(
'.variable_subscription_trial_sign_up'
),
$subscriptionPricingRow = $( this ).siblings(
'.variable_subscription_pricing'
);
// Position gifting field after trial sign up row if it exists, otherwise after subscription pricing
if ( $trialSignUpRow.length > 0 ) {
$( this ).insertAfter( $trialSignUpRow );
} else if ( $subscriptionPricingRow.length > 0 ) {
$( this ).insertAfter( $subscriptionPricingRow );
}
$( this ).addClass( 'wcs_gifting_moved' );
} );
},
getVariationBulkEditValue: function ( variation_action ) {
var value;
@@ -632,18 +653,27 @@ jQuery( function ( $ ) {
$( '.options_group.subscription_pricing' )
);
// Move the subscription variation pricing section to a better location in the DOM on load
// Move the subscription variation pricing and gifting sections to a better location in the DOM on load
// We do this because these sections are initially loaded at the end of the variable product options section,
// which is not the best location for them, so we move to before the "Sale price" section.
if (
$( '#variable_product_options .variable_subscription_pricing' ).length >
0
) {
$.moveSubscriptionVariationFields();
}
if (
$( '#variable_product_options .variable_subscription_gifting' ).length >
0
) {
$.moveSubscriptionGiftingFields();
}
// When a variation is added
$( '#woocommerce-product-data' ).on(
'woocommerce_variations_added woocommerce_variations_loaded',
function () {
$.moveSubscriptionVariationFields();
$.moveSubscriptionGiftingFields();
$.showHideVariableSubscriptionMeta();
$.showHideSyncOptions();
$.setSubscriptionLengths();
@@ -1030,6 +1060,33 @@ jQuery( function ( $ ) {
} );
}
var $giftingEnableCheckbox = $(
document.getElementById( 'woocommerce_subscriptions_gifting_enable_gifting' )
),
$giftingRadios = $(
'.wc-settings-row-gifting-radios'
);
if ( $giftingEnableCheckbox.length > 0 ) {
function toggleGiftingCheckbox( checked ) {
if ( checked ) {
$giftingRadios.show();
$giftingEnableCheckbox.closest( 'tr' ).addClass( 'checked' );
return;
}
$giftingRadios.hide();
$giftingEnableCheckbox.closest( 'tr' ).removeClass( 'checked' );
}
$giftingEnableCheckbox.on( 'change', function() {
toggleGiftingCheckbox( this.checked );
} );
toggleGiftingCheckbox( $giftingEnableCheckbox.is(':checked') );
}
// Don't display the variation notice for variable subscription products
$( 'body' ).on( 'woocommerce-display-product-type-alert', function (
e,

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -5,18 +5,30 @@ jQuery( function ( $ ) {
$( '.wcs_deletion_error' ).on( 'click', function ( e ) {
e.preventDefault();
var notice_content_container = $( '#wcs_delete_token_warning' ).find( 'li' );
var notice_content_container = $( '#wcs_delete_token_warning' ).find(
'li'
);
// For block based WC notices we need to find the notice content container.
if ( $( '#wcs_delete_token_warning' ).find( '.wc-block-components-notice-banner' ).length > 0 ) {
notice_content_container = $( '#wcs_delete_token_warning' ).find( '.wc-block-components-notice-banner__content' );
if (
$( '#wcs_delete_token_warning' ).find(
'.wc-block-components-notice-banner'
).length > 0
) {
notice_content_container = $( '#wcs_delete_token_warning' ).find(
'.wc-block-components-notice-banner__content'
);
}
// Use the href to determine which notice needs to be displayed.
if ( '#choose_default' === $( this ).attr( 'href' ) ) {
notice_content_container.html( wcs_payment_methods.choose_default_error );
notice_content_container.html(
wcs_payment_methods.choose_default_error
);
} else {
notice_content_container.html( wcs_payment_methods.add_method_error );
notice_content_container.html(
wcs_payment_methods.add_method_error
);
}
// Display the notice.

View File

@@ -165,5 +165,9 @@ jQuery( function ( $ ) {
$early_renewal_modal_submit.on( 'click', blockEarlyRenewalModal );
$( document ).on( 'wcs_show_modal', shouldShowEarlyRenewalModal );
$( document ).on( 'click', '.wcs_block_ui_on_click', blockActionsOnTrigger );
$( document ).on(
'click',
'.wcs_block_ui_on_click',
blockActionsOnTrigger
);
} );

View File

@@ -26,10 +26,10 @@ jQuery( function ( $ ) {
$( document ).on(
'change',
'select.shipping_method, :input[name^=shipping_method]',
function( event ) {
function ( event ) {
var shipping_method_option = $( event.target );
var shipping_method_id = shipping_method_option.val();
var package_index = shipping_method_option.data( 'index' );
var shipping_method_id = shipping_method_option.val();
var package_index = shipping_method_option.data( 'index' );
// We're only interested in the initial cart shipping method options which have int package indexes.
if ( ! Number.isInteger( package_index ) ) {
@@ -37,9 +37,17 @@ jQuery( function ( $ ) {
}
// Find all recurring cart info elements with the same package index as the changed shipping method.
$( '.recurring-cart-shipping-mapping-info[data-index=' + package_index + ']' ).each( function() {
$(
'.recurring-cart-shipping-mapping-info[data-index=' +
package_index +
']'
).each( function () {
// Update the corresponding subscription's hidden chosen shipping method.
$( 'input[name="shipping_method[' + $( this ).data( 'recurring_index' ) + ']"]' ).val( shipping_method_id );
$(
'input[name="shipping_method[' +
$( this ).data( 'recurring_index' ) +
']"]'
).val( shipping_method_id );
} );
}
);

View File

@@ -0,0 +1,355 @@
jQuery( document ).ready( function ( $ ) {
setShippingAddressNoticeVisibility( true );
$( document ).on(
'change',
'.woocommerce_subscription_gifting_checkbox[type="checkbox"]',
function ( e, eventContext ) {
if ( $( this ).is( ':checked' ) ) {
$( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.wcsg_add_recipient_fields' )
.slideDown( 250, function () {
if (
typeof eventContext === 'undefined' ||
eventContext !== 'pageload'
) {
$( this )
.find( '.recipient_email' )
.trigger( 'focus' );
}
} );
const shipToDifferentAddressCheckbox = $( document ).find(
'#ship-to-different-address-checkbox'
);
if ( ! shipToDifferentAddressCheckbox.is( ':checked' ) ) {
shipToDifferentAddressCheckbox.click();
}
setShippingAddressNoticeVisibility( false );
} else {
$( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.wcsg_add_recipient_fields' )
.slideUp( 250 );
const recipientEmailElement = $( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.recipient_email' );
recipientEmailElement.val( '' );
setShippingAddressNoticeVisibility( true );
if ( $( 'form.checkout' ).length !== 0 ) {
// Trigger the event to update the checkout after the recipient field has been cleared.
updateCheckout();
}
}
}
);
/**
* Handles showing and hiding the gifting checkbox on variable subscription products.
*/
function hideGiftingCheckbox() {
$( '.woocommerce_subscription_gifting_checkbox[type="checkbox"]' )
.prop( 'checked', false )
.trigger( 'change' );
$( '.wcsg_add_recipient_fields_container' ).hide();
}
// When a variation is found, show the gifting checkbox if it's enabled for the variation, otherwise hide it.
$( document ).on( 'found_variation', function ( event, variationData ) {
if ( variationData.gifting ) {
$( '.wcsg_add_recipient_fields_container' ).show();
return;
}
hideGiftingCheckbox();
} );
// When the data is reset, hide the gifting checkbox.
$( document ).on( 'reset_data', hideGiftingCheckbox );
/**
* Handles recipient e-mail inputs on the cart page.
*/
const cart = {
init: function () {
$( document ).on(
'submit',
'div.woocommerce > form',
this.set_update_cart_as_clicked
);
// We need to make sure our callback is hooked before WC's.
const handlers = $._data( document, 'events' );
if ( typeof handlers.submit !== 'undefined' ) {
handlers.submit.unshift( handlers.submit.pop() );
}
},
set_update_cart_as_clicked: function ( evt ) {
const $form = $( evt.target );
// eslint-disable-next-line no-restricted-globals
const $submit = $( document.activeElement );
// If we're not on the cart page exit.
if ( $form.find( 'table.shop_table.cart' ).length === 0 ) {
return;
}
// If the recipient email element is the active element, the clicked button is the update cart button.
if ( $submit.is( 'input.recipient_email' ) ) {
$( ':input[type="submit"][name="update_cart"]' ).attr(
'clicked',
'true'
);
}
},
};
cart.init();
/**
* Email validation function
*
* @param {string} email - The email to validate
* @return {boolean} - Whether the email is valid
*/
function isValidEmail( email ) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test( email );
}
/**
* Validate all recipient emails and return overall validation status
*
* @param {boolean} showErrors - Whether to show validation errors
* @return {boolean} - Whether all emails are valid
*/
function validateAllRecipientEmails( showErrors = true ) {
const $allEmailFields = $( '.recipient_email' );
let allValid = true;
// Check each email field
$allEmailFields.each( function () {
const $emailField = $( this );
const $giftingCheckbox = $( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.woocommerce_subscription_gifting_checkbox' );
const email = $emailField.val().trim();
if ( ! $giftingCheckbox.is( ':checked' ) ) {
return;
}
// Check if email format is valid
if ( ! isValidEmail( email ) ) {
if ( showErrors ) {
showValidationErrorForEmailField( $emailField );
}
allValid = false;
}
} );
// Control update cart button state
const $updateCartButton = $(
'.woocommerce-cart-form :input[type="submit"][name="update_cart"]'
);
if ( $updateCartButton.length ) {
$updateCartButton.prop( 'disabled', ! allValid );
}
return allValid;
}
/**
* Validate recipient email and show error if invalid
*
* @param {jQuery} $emailField - The email input field jQuery object
* @return {boolean} - Whether the email is valid
*/
function validateRecipientEmail( $emailField ) {
const email = $emailField.val().trim();
hideValidationErrorForEmailField( $emailField );
// Check if email format is valid
if ( ! isValidEmail( email ) ) {
showValidationErrorForEmailField( $emailField );
// Only validate all emails and update button state on cart and checkout shortcode pages.
if ( isShortcodeCartOrCheckoutPage() ) {
validateAllRecipientEmails();
}
return false;
}
// Only validate all emails and update button state on cart and checkout shortcode pages.
if ( isShortcodeCartOrCheckoutPage() ) {
validateAllRecipientEmails();
}
return true;
}
/**
* Handle add to cart button click with email validation
*/
$( document ).on(
'click',
'.single_add_to_cart_button, .add_to_cart_button',
function ( e ) {
// Check if we're on a product page with gifting enabled
const $giftingContainer = $(
'.wcsg_add_recipient_fields_container'
);
if ( $giftingContainer.length === 0 ) {
return; // No gifting on this page
}
// Check if gifting checkbox is checked
const $giftingCheckbox = $giftingContainer.find(
'.woocommerce_subscription_gifting_checkbox'
);
if ( ! $giftingCheckbox.is( ':checked' ) ) {
return; // Gifting not enabled for this item
}
// Get the recipient email field
const $emailField = $giftingContainer.find( '.recipient_email' );
if ( $emailField.length === 0 ) {
return; // No email field found
}
// Validate the email
if ( ! validateRecipientEmail( $emailField ) ) {
e.preventDefault();
e.stopPropagation();
// Focus on the email field
$emailField.focus();
return false;
}
}
);
/**
* Real-time email validation on input
*/
$( document ).on( 'blur', '.recipient_email', function () {
const $emailField = $( this );
validateRecipientEmail( $emailField );
} );
/**
* Clear error styling when user starts typing
*/
$( document ).on( 'input', '.recipient_email', function () {
const $emailField = $( this );
hideValidationErrorForEmailField( $emailField );
} );
/*******************************************
* Update checkout on input changed events *
*******************************************/
let updateTimer;
$( document ).on( 'change', '.recipient_email', function () {
if ( $( 'form.checkout' ).length === 0 ) {
return;
}
if ( validateAllRecipientEmails() ) {
updateCheckout();
}
} );
$( document ).on( 'keyup', '.recipient_email', function ( e ) {
const code = e.keyCode || e.which || 0;
if ( $( 'form.checkout' ).length === 0 || code === 9 ) {
return true;
}
const currentRecipient = $( this ).val();
const originalRecipient = $( this ).attr( 'data-recipient' );
resetCheckoutUpdateTimer();
// If the recipient has changed since last load, mark the element as needing an update.
if ( currentRecipient !== originalRecipient ) {
$( this ).addClass( 'wcsg_needs_update' );
// Only set timer if all emails are valid
if ( validateAllRecipientEmails( false ) ) {
updateTimer = setTimeout( updateCheckout, 1500 );
}
} else {
$( this ).removeClass( 'wcsg_needs_update' );
}
} );
function updateCheckout() {
resetCheckoutUpdateTimer();
$( '.recipient_email' ).removeClass( 'wcsg_needs_update' );
$( document.body ).trigger( 'update_checkout' );
}
function resetCheckoutUpdateTimer() {
clearTimeout( updateTimer );
}
function setShippingAddressNoticeVisibility( hide = true ) {
const notice = $( 'form.checkout' )
.find( '.woocommerce-shipping-fields' )
.find( '.woocommerce-info' );
if ( ! notice.length ) {
return;
}
if ( hide ) {
notice.css( { display: 'none' } );
} else {
notice.css( { display: '' } );
}
}
function isShortcodeCartOrCheckoutPage() {
return (
$( 'form.woocommerce-cart-form' ).length > 0 ||
$( 'form.woocommerce-checkout' ).length > 0
);
}
function showValidationErrorForEmailField( $emailField ) {
$emailField.addClass( 'wcsg-email-error' );
$emailField
.closest( '.wcsg_add_recipient_fields' )
.find( '.wc-shortcode-components-validation-error' )
.show();
}
function hideValidationErrorForEmailField( $emailField ) {
$emailField.removeClass( 'wcsg-email-error' );
$emailField
.closest( '.wcsg_add_recipient_fields' )
.find( '.wc-shortcode-components-validation-error' )
.hide();
}
// Triggers
$( '.woocommerce_subscription_gifting_checkbox[type="checkbox"]' ).trigger(
'change',
'pageload'
);
// Validate all recipient emails on page load to set initial button state
$( document ).ready( function () {
setTimeout( function () {
// Only run validation on cart and checkout shortcode pages
if ( isShortcodeCartOrCheckoutPage() ) {
validateAllRecipientEmails();
}
}, 1000 );
} );
} );

View File

@@ -0,0 +1,56 @@
jQuery( document ).ready( function ( $ ) {
// Remove WC's revoke handler to make sure that only our handler is called (to make sure only the correct permissions are revoked not all permissions matching the product/order ID)
$( '.order_download_permissions' ).off( 'click', 'button.revoke_access' );
$( '.order_download_permissions' ).on(
'click',
'button.revoke_access',
function () {
if (
window.confirm(
woocommerce_admin_meta_boxes.i18n_permission_revoke
)
) {
var el = $( this ).parent().parent();
var permission_id = $( this )
.siblings()
.find( '.wcsg_download_permission_id' )
.val();
var post_id = $( '#post_ID' ).val();
if ( 0 < permission_id ) {
$( el ).block( {
message: null,
overlayCSS: {
background: '#fff',
opacity: 0.6,
},
} );
var data = {
action: 'wcsg_revoke_access_to_download',
post_id: post_id,
download_permission_id: permission_id,
nonce: wcs_gifting.revoke_download_permission_nonce,
};
$.ajax( {
url: wcs_gifting.ajax_url,
data: data,
type: 'POST',
success: function () {
// Success
$( el ).fadeOut( '300', function () {
$( el ).remove();
} );
},
} );
} else {
$( el ).fadeOut( '300', function () {
$( el ).remove();
} );
}
}
}
);
} );

View File

@@ -1,134 +0,0 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { registerCheckoutFilters } from '@woocommerce/blocks-checkout';
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import {
getSwitchString,
isOneOffSubscription,
getBillingFrequencyString,
} from '../utils';
/**
* This is the filter integration API, it uses registerCheckoutFilters
* to register its filters, each filter is a key: function pair.
* The key the filter name, and the function is the filter.
*
* Each filter function is passed the previous (or default) value in that filter
* as the first parameter, the second parameter is a object of 3PD registered data.
* For WCS, we register out data with key `subscriptions`.
* Filters must return the previous value or a new value with the same type.
* If an error is thrown, it would be visible for store managers only.
*/
export const registerFilters = () => {
registerCheckoutFilters( 'woocommerce-subscriptions', {
// subscriptions data here comes from register_endpoint_data /cart registration.
totalLabel: ( label, { subscriptions } ) => {
if ( 0 < subscriptions?.length ) {
return __( 'Total due today', 'woocommerce-subscriptions' );
}
return label;
},
// subscriptions data here comes from register_endpoint_data /cart/items registration.
subtotalPriceFormat: ( label, { subscriptions } ) => {
if (
subscriptions?.billing_period &&
subscriptions?.billing_interval
) {
const {
billing_interval: billingInterval,
subscription_length: subscriptionLength,
} = subscriptions;
// We check if we have a length and its equal or less to the billing interval.
// When this is true, it means we don't have a next payment date.
if (
isOneOffSubscription( {
subscriptionLength,
billingInterval,
} )
) {
// An edge case when length is 1 so it doesn't have a length prefix
if ( 1 === subscriptionLength ) {
return getBillingFrequencyString(
subscriptions,
// translators: the word used to describe billing frequency, e.g. "for" 1 day or "for" 1 month.
__( 'for 1', 'woocommerce-subscriptions' ),
label
);
}
return getBillingFrequencyString(
subscriptions,
// translators: the word used to describe billing frequency, e.g. "for" 6 days or "for" 2 weeks.
__( 'for', 'woocommerce-subscriptions' ),
label
);
}
return getBillingFrequencyString(
subscriptions,
// translators: the word used to describe billing frequency, e.g. "every" 6 days or "every" 2 weeks.
__( 'every', 'woocommerce-subscriptions' ),
label
);
}
return label;
},
saleBadgePriceFormat: ( label, { subscriptions } ) => {
if (
subscriptions?.billing_period &&
subscriptions?.billing_interval
) {
return getBillingFrequencyString( subscriptions, '/', label );
}
return label;
},
itemName: ( name, { subscriptions } ) => {
if ( subscriptions?.is_resubscribe ) {
return sprintf(
// translators: %s Product name.
__( '%s (resubscription)', 'woocommerce-subscriptions' ),
name
);
}
if ( subscriptions?.switch_type ) {
return sprintf(
// translators: %1$s Product name, %2$s Switch type (upgraded, downgraded, or crossgraded).
__( '%1$s (%2$s)', 'woocommerce-subscriptions' ),
name,
getSwitchString( subscriptions.switch_type )
);
}
return name;
},
cartItemPrice: ( pricePlaceholder, { subscriptions }, { context } ) => {
if ( subscriptions?.sign_up_fees ) {
return 'cart' === context
? sprintf(
/* translators: %s is the subscription price to pay immediately (ie: $10). */
__( 'Due today %s', 'woocommerce-subscriptions' ),
pricePlaceholder
)
: sprintf(
/* translators: %s is the subscription price to pay immediately (ie: $10). */
__( '%s due today', 'woocommerce-subscriptions' ),
pricePlaceholder
);
}
return pricePlaceholder;
},
placeOrderButtonLabel: ( label ) => {
const subscriptionsData = getSetting( 'subscriptions_data' );
if ( subscriptionsData?.place_order_override ) {
return subscriptionsData?.place_order_override;
}
return label;
},
} );
};

View File

@@ -1,49 +0,0 @@
/**
* External dependencies
*/
import { registerPlugin } from '@wordpress/plugins';
import {
ExperimentalOrderMeta,
ExperimentalOrderShippingPackages,
} from '@woocommerce/blocks-checkout';
/**
* Internal dependencies
*/
import { SubscriptionsRecurringTotals } from './recurring-totals';
import { SubscriptionsRecurringPackages } from './recurring-packages';
import { registerFilters } from './filters';
import './index.scss';
/**
* This is the first integration point between WooCommerce Subscriptions
* and Cart and Checkout blocks, it happens on two folds:
* - First, we register our code via `registerPlugin`, this React code
* is then going to be rendered hidden inside Cart and Checkout blocks
* (via <PluginArea /> component).
* - Second, we're using SlotFills[1] to move that code to where we want it
* inside the tree.
*/
const render = () => {
return (
<>
<ExperimentalOrderShippingPackages>
<SubscriptionsRecurringPackages />
</ExperimentalOrderShippingPackages>
<ExperimentalOrderMeta>
<SubscriptionsRecurringTotals />
</ExperimentalOrderMeta>
</>
);
};
registerPlugin( 'woocommerce-subscriptions', {
render,
scope: 'woocommerce-checkout',
} );
/**
* RegisterFilters is the second part of the integration, and it handles filters
* like price, totals, and so on.
*/
registerFilters();

View File

@@ -1 +0,0 @@
// Add styles here.

View File

@@ -1,66 +0,0 @@
/**
* External dependencies
*/
import { useMemo } from '@wordpress/element';
/**
* This component is responsible for rending recurring shippings.
* It has to be the highest level item directly inside the SlotFill
* to receive properties passed from Cart and Checkout.
*
* extensions is data registered into `/cart` endpoint.
*
* @param {Object} props Passed props from SlotFill to this component.
* @param {Object} props.extensions Data registered into `/cart` endpoint.
* @param {boolean} props.collapsible If shipping rates can collapse.
* @param {boolean} props.collapse If shipping rates should collapse.
* @param {boolean} props.showItems If shipping rates should show items inside them.
* @param {Element} props.noResultsMessage Message shown when no rate are found.
* @param {Function} props.renderOption Function that decides how rates are going to render.
* @param {Object} props.components
* @param {string} props.context This will be woocommerce/cart or woocommerce/checkout.
*/
export const SubscriptionsRecurringPackages = ( {
extensions,
collapsible,
collapse,
showItems,
noResultsMessage,
renderOption,
components,
context,
} ) => {
const { subscriptions = [] } = extensions;
const { ShippingRatesControlPackage } = components;
// Flatten all packages from recurring carts.
const packages = useMemo(
() =>
Object.values( subscriptions )
.map( ( recurringCart ) => recurringCart.shipping_rates )
.filter( Boolean )
.flat(),
[ subscriptions ]
);
const shouldCollapse = useMemo( () => 1 < packages.length || collapse, [
packages.length,
collapse,
] );
const shouldShowItems = useMemo( () => 1 < packages.length || showItems, [
packages.length,
showItems,
] );
return packages.map( ( { package_id: packageId, ...packageData } ) => (
<ShippingRatesControlPackage
key={ packageId }
packageId={ packageId }
packageData={ packageData }
collapsible={ collapsible }
collapse={ shouldCollapse }
showItems={ shouldShowItems }
noResultsMessage={ noResultsMessage }
renderOption={ renderOption }
highlightChecked={ 'woocommerce/checkout' === context }
/>
) );
};

View File

@@ -1,319 +0,0 @@
/**
* External dependencies
*/
import { sprintf, __ } from '@wordpress/i18n';
import {
Panel,
Subtotal,
TotalsItem,
TotalsTaxes,
TotalsWrapper,
} from '@woocommerce/blocks-checkout';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { isWcVersion, getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import {
getRecurringPeriodString,
getSubscriptionLengthString,
isOneOffSubscription,
} from '../utils';
import './index.scss';
/**
* All data passed in get_script_data is available here, from all
* plugins (e.g WooCommerce Admin, WooCommerce Blocks).
*/
const DISPLAY_CART_PRICES_INCLUDING_TAX = getSetting(
'displayCartPricesIncludingTax',
false
);
/**
* Component responsible for rending the coupons discount totals item.
*
* @param {Object} props Props passed to component.
* @param {Object} props.currency Object containing currency data to format prices.
* @param {Object} props.values Recurring cart totals (shipping, taxes).
*/
const DiscountTotals = ( { currency, values } ) => {
const {
total_discount: totalDiscount,
total_discount_tax: totalDiscountTax,
} = values;
const discountValue = parseInt( totalDiscount, 10 );
if ( ! discountValue ) {
return null;
}
const discountTaxValue = parseInt( totalDiscountTax, 10 );
const discountTotalValue = DISPLAY_CART_PRICES_INCLUDING_TAX
? discountValue + discountTaxValue
: discountValue;
return (
<TotalsItem
className="wc-block-components-totals-discount"
currency={ currency }
label={ __( 'Discount', 'woocommerce-subscriptions' ) }
value={ discountTotalValue * -1 }
/>
);
};
/**
* Component responsible for rending the shipping totals item.
*
* @param {Object} props Props passed to component.
* @param {string|undefined} props.selectedRate Selected shipping method
* name.
* @param {boolean} props.needsShipping Boolean to indicate if we
* need shipping or not.
* @param {boolean} props.calculatedShipping Boolean to indicate if we
* calculated shipping or not.
* @param {Object} props.currency Object containing
* currency data to format prices.
* @param {Object} props.values Recurring cart totals (shipping, taxes).
*/
const ShippingTotal = ( {
values,
currency,
selectedRate,
needsShipping,
calculatedShipping,
} ) => {
if ( ! needsShipping || ! calculatedShipping ) {
return null;
}
const shippingTotals = DISPLAY_CART_PRICES_INCLUDING_TAX
? parseInt( values.total_shipping, 10 ) +
parseInt( values.total_shipping_tax, 10 )
: parseInt( values.total_shipping, 10 );
const valueToShow =
0 === shippingTotals && isWcVersion( '9.0', '>=' ) ? (
<strong>{ __( 'Free', 'woocommerce-subscriptions' ) }</strong>
) : (
shippingTotals
);
return (
<TotalsItem
value={ valueToShow }
label={ __( 'Shipping', 'woocommerce-subscriptions' ) }
currency={ currency }
description={
!! selectedRate &&
sprintf(
// translators: %s selected shipping rate (ex: flat rate)
__( 'via %s', 'woocommerce-subscriptions' ),
selectedRate
)
}
/>
);
};
/**
* Component responsible for rendering recurring cart description.
*
* @param {Object} props Props passed to component.
* @param {string} props.nextPaymentDate Formatted next payment date.
* @param {number} props.subscriptionLength Subscription length.
* @param {string} props.billingPeriod Recurring cart period (day, week, month, year).
* @param {number} props.billingInterval Recurring cart interval (1 - 6).
*/
const SubscriptionDescription = ( {
nextPaymentDate,
subscriptionLength,
billingPeriod,
billingInterval,
} ) => {
const subscriptionLengthString = getSubscriptionLengthString( {
subscriptionLength,
billingPeriod,
} );
const firstPaymentString = isOneOffSubscription( {
subscriptionLength,
billingInterval,
} )
? sprintf(
/* Translators: %1$s is a date. */
__( 'Due: %1$s', 'woocommerce-subscriptions' ),
nextPaymentDate
)
: sprintf(
/* Translators: %1$s is a date. */
__( 'Starting: %1$s', 'woocommerce-subscriptions' ),
nextPaymentDate
);
return (
// Only render this section if we have a next payment date.
<span>
{ !! nextPaymentDate && firstPaymentString }{ ' ' }
{ !! subscriptionLength &&
subscriptionLength >= billingInterval && (
<span className="wcs-recurring-totals__subscription-length">
{ subscriptionLengthString }
</span>
) }
</span>
);
};
/**
* Component responsible for rendering recurring cart heading.
*
* @param {Object} props Props passed to component.
* @param {Object} props.currency Object containing currency data to format prices.
* @param {number} props.billingInterval Recurring cart interval (1 - 6).
* @param {string} props.billingPeriod Recurring cart period (day, week, month, year).
* @param {string} props.nextPaymentDate Formatted next payment date.
* @param {number} props.subscriptionLength Subscription length.
* @param {Object} props.totals Recurring cart totals (shipping, taxes).
*/
const TabHeading = ( {
currency,
billingInterval,
billingPeriod,
nextPaymentDate,
subscriptionLength,
totals,
} ) => {
// For future one off subscriptions, we show "Total" instead of a recurring title.
const title = isOneOffSubscription( {
billingInterval,
subscriptionLength,
} )
? __( 'Total', 'woocommerce-subscriptions' )
: getRecurringPeriodString( {
billingInterval,
billingPeriod,
} );
return (
<TotalsItem
className="wcs-recurring-totals-panel__title"
currency={ currency }
label={ title }
value={ totals }
description={
<SubscriptionDescription
nextPaymentDate={ nextPaymentDate }
subscriptionLength={ subscriptionLength }
billingInterval={ billingInterval }
billingPeriod={ billingPeriod }
/>
}
/>
);
};
/**
* Component responsible for rendering a single recurring total panel.
* We render several ones depending on how many recurring carts we have.
*
* @param {Object} props Props passed to component.
* @param {Object} props.subscription Recurring cart data that we registered
* with ExtendRestApi.
* @param {boolean} props.needsShipping Boolean to indicate if we need
* shipping or not.
* @param {boolean} props.calculatedShipping Boolean to indicate if we calculated
* shipping or not.
*/
const RecurringSubscription = ( {
subscription,
needsShipping,
calculatedShipping,
} ) => {
const {
totals,
billing_interval: billingInterval,
billing_period: billingPeriod,
next_payment_date: nextPaymentDate,
subscription_length: subscriptionLength,
shipping_rates: shippingRates,
} = subscription;
// We skip one off subscriptions
if ( ! nextPaymentDate ) {
return null;
}
const selectedRate = shippingRates?.[ 0 ]?.shipping_rates?.find(
( { selected } ) => selected
)?.name;
const currency = getCurrencyFromPriceResponse( totals );
return (
<div className="wcs-recurring-totals-panel">
<TabHeading
billingInterval={ billingInterval }
billingPeriod={ billingPeriod }
nextPaymentDate={ nextPaymentDate }
subscriptionLength={ subscriptionLength }
totals={ parseInt( totals.total_price, 10 ) }
currency={ currency }
/>
<Panel
className="wcs-recurring-totals-panel__details"
initialOpen={ false }
title={ __( 'Details', 'woocommerce-subscriptions' ) }
>
<TotalsWrapper>
<Subtotal currency={ currency } values={ totals } />
<DiscountTotals currency={ currency } values={ totals } />
</TotalsWrapper>
<TotalsWrapper className="wc-block-components-totals-shipping">
<ShippingTotal
currency={ currency }
needsShipping={ needsShipping }
calculatedShipping={ calculatedShipping }
values={ totals }
selectedRate={ selectedRate }
/>
</TotalsWrapper>
{ ! DISPLAY_CART_PRICES_INCLUDING_TAX && (
<TotalsWrapper>
<TotalsTaxes currency={ currency } values={ totals } />
</TotalsWrapper>
) }
<TotalsWrapper>
<TotalsItem
className="wcs-recurring-totals-panel__details-total"
currency={ currency }
label={ __( 'Total', 'woocommerce-subscriptions' ) }
value={ parseInt( totals.total_price, 10 ) }
/>
</TotalsWrapper>
</Panel>
</div>
);
};
/**
* This component is responsible for rending recurring totals.
* It has to be the highest level item directly inside the SlotFill
* to receive properties passed from Cart and Checkout.
*
* extensions is data registered into `/cart` endpoint.
*
* @param {Object} props Passed props from SlotFill to this component.
* @param {Object} props.extensions data registered into `/cart` endpoint.
* @param {Object} props.cart cart endpoint data in readonly mode.
*/
export const SubscriptionsRecurringTotals = ( { extensions, cart } ) => {
const { subscriptions } = extensions;
const { cartNeedsShipping, cartHasCalculatedShipping } = cart;
if ( ! subscriptions || 0 === subscriptions.length ) {
return null;
}
return subscriptions.map( ( { key, ...subscription } ) => (
<RecurringSubscription
subscription={ subscription }
needsShipping={ cartNeedsShipping }
calculatedShipping={ cartHasCalculatedShipping }
key={ key }
/>
) );
};

View File

@@ -1,72 +0,0 @@
// Shows a border with the current color and a custom opacity. That can't be achieved
// with normal border because `currentColor` doesn't allow tweaking the opacity, and
// setting the opacity of the entire element would change the children's opacity too.
@mixin with-translucent-border( $border-width: 1px, $opacity: 0.3 ) {
position: relative;
&::after {
border-style: solid;
border-width: $border-width;
bottom: 0;
content: '';
display: block;
left: 0;
opacity: $opacity;
pointer-events: none;
position: absolute;
right: 0;
top: 0;
}
}
.wcs-recurring-totals-panel {
@include with-translucent-border( 1px 0 );
padding: 1em 0 0;
+ .wcs-recurring-totals-panel::after {
border-top-width: 0;
}
.wc-block-components-panel .wc-block-components-totals-item {
padding-left: 0;
padding-right: 0;
}
.wc-block-components-totals-item__label::first-letter {
text-transform: capitalize;
}
.wcs-recurring-totals-panel__title .wc-block-components-totals-item__label {
font-weight: 700;
}
}
.wcs-recurring-totals-panel__title {
margin: 0;
}
.wcs-recurring-totals-panel__details {
.wc-block-components-panel__button,
.wc-block-components-panel__button:hover,
.wc-block-components-panel__button:focus {
font-size: 0.875em;
}
.wc-block-components-panel__content > .wc-block-components-totals-item {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
.wcs-recurring-totals-panel__details-total
.wc-block-components-totals-item__label {
font-weight: 700;
}
}
.wcs-recurring-totals__subscription-length {
float: right;
}

View File

@@ -1,207 +0,0 @@
/**
* External dependencies
*/
import { sprintf, __, _nx } from '@wordpress/i18n';
export function getAvailablePeriods( number ) {
return {
day: _nx(
'day',
'days',
number,
'Used in recurring totals section in Cart. 2+ will need plural, 1 will need singular.',
'woocommerce-subscriptions'
),
week: _nx(
'week',
'weeks',
number,
'Used in recurring totals section in Cart. 2+ will need plural, 1 will need singular.',
'woocommerce-subscriptions'
),
month: _nx(
'month',
'months',
number,
'Used in recurring totals section in Cart. 2+ will need plural, 1 will need singular.',
'woocommerce-subscriptions'
),
year: _nx(
'year',
'years',
number,
'Used in recurring totals section in Cart. 2+ will need plural, 1 will need singular.',
'woocommerce-subscriptions'
),
};
}
/**
* Creates a recurring string from a subscription
*
* Examples
* period recurring total
* Daily recurring total
* Weekly recurring total
* Monthly recurring total
* etc
* If subscription bills at non standard intervals, then the order is transposed, and the line reads:
* Recurring total every X day | week | month | quarter | year
* Recurring total every 3rd day
* Recurring total every 2nd week
* Recurring total every 4th month
* etc
*
* @param {Object} subscription Subscription object.
* @param {string} subscription.billingPeriod Period (month, day, week, year).
* @param {number} subscription.billingInterval Internal (1 month, 5 day, 4 week, 6 year).
*/
export function getRecurringPeriodString( { billingInterval, billingPeriod } ) {
switch ( billingInterval ) {
case 1:
if ( 'day' === billingPeriod ) {
return __(
'Daily recurring total',
'woocommerce-subscriptions'
);
} else if ( 'week' === billingPeriod ) {
return __(
'Weekly recurring total',
'woocommerce-subscriptions'
);
} else if ( 'month' === billingPeriod ) {
return __(
'Monthly recurring total',
'woocommerce-subscriptions'
);
} else if ( 'year' === billingPeriod ) {
return __(
'Yearly recurring total',
'woocommerce-subscriptions'
);
}
break;
case 2:
return sprintf(
/* translators: %1$s is week, month, year */
__(
'Recurring total every 2nd %1$s',
'woocommerce-subscriptions'
),
billingPeriod
);
case 3:
return sprintf(
/* Translators: %1$s is week, month, year */
__(
'Recurring total every 3rd %1$s',
'woocommerce-subscriptions'
),
billingPeriod
);
default:
return sprintf(
/* Translators: %1$d is number of weeks, months, days, years. %2$s is week, month, year */
__(
'Recurring total every %1$dth %2$s',
'woocommerce-subscriptions'
),
billingInterval,
billingPeriod
);
}
}
export function getSubscriptionLengthString( {
subscriptionLength,
billingPeriod,
} ) {
const periodsStings = getAvailablePeriods( subscriptionLength );
return sprintf(
'For %1$d %2$s',
subscriptionLength,
periodsStings[ billingPeriod ],
'woocommerce-subscriptions'
);
}
/**
* Creates a billing frequency string from a subscription
*
* Examples
* Every 6th week
* Every day
* Every month
* / day
* Each Week
* etc
*
* @param {Object} subscription Subscription object.
* @param {string} subscription.billing_period Period (month, day, week, year).
* @param {number} subscription.billing_interval Internal (1 month, 5 day, 4 week, 6 year).
* @param {string} separator A string to be prepended to frequency. followed by a space. Eg: (every, each, /)
* @param {string} price This is the string representation of the price of the product.
*/
export function getBillingFrequencyString(
{ billing_interval: billingInterval, billing_period: billingPeriod },
separator,
price
) {
const periodsStings = getAvailablePeriods( billingInterval );
const translatedPeriod = periodsStings[ billingPeriod ];
separator = separator.trim();
switch ( billingInterval ) {
case 1:
return `${ price } ${ separator } ${ translatedPeriod }`;
default:
return sprintf(
/*
* translators: %1$s is the price of the product. %2$s is the separator used e.g "every" or "/",
* %3$d is the length, %4$s is week, month, year
*/
__( `%1$s %2$s %3$d %4$s`, 'woocommerce-subscriptions' ),
price,
separator,
billingInterval,
translatedPeriod
);
}
}
/**
* Returns a switch string
*
* @param {string} switchType The switch type (upgraded, downgraded, crossgraded).
*
* @return {string} Translation ready switch name.
*/
export function getSwitchString( switchType ) {
switch ( switchType ) {
case 'upgraded':
return __( 'Upgrade', 'woocommerce-subscriptions' );
case 'downgraded':
return __( 'Downgrade', 'woocommerce-subscriptions' );
case 'crossgraded':
return __( 'Crossgrade', 'woocommerce-subscriptions' );
default:
return '';
}
}
/**
* Checks weather a subscription is a one off or not.
*
* @param {Object} subscription Subscription object data.
* @param {number} subscription.subscriptionLength Subscription length.
* @param {number} subscription.billingInterval Billing interval
* @return {boolean} whether this is a one off subscription or not.
*/
export function isOneOffSubscription( {
subscriptionLength,
billingInterval,
} ) {
return subscriptionLength === billingInterval;
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -1,5 +1,16 @@
*** WooCommerce Subscriptions Changelog ***
2025-08-19 - version 7.8.0
* Add: Native support for subscriptions gifting. Gifting for WooCommerce Subscriptions extension is no longer required.
* Add: Enable subscriptions gifting storewide and per product.
* Add: Blocks support for gifting on product, cart, and checkout pages.
* Update: Make WooCommerce Subscriptions reports compatible with High Performance Order Storage.
* Update: Rename Subscribe now button to Add to Cart to follow WooCommerce convention.
* Fix: Fix integration with WooCommerce dashboard widget.
* Fix: Cancel pending related orders when a subscription is cancelled to prevent orphaned orders that need payment.
* Fix: Allow manual payments for pending renewal orders of Product Bundles or Composite Products when Mixed Checkout options is disabled.
* Dev: Update moment.js package to the latest version 2.30.1
2025-07-09 - version 7.7.0
* Fix: Restores normal behavior for the report caching updates scheduled action, which was failing due to a bad filepath.
* Fix: Fix error when placing an order with a valid card after using a declined one.

View File

@@ -23,10 +23,13 @@ class WCS_Admin_Reports {
* Constructor
*/
public function __construct() {
// The subscription reports are incompatible with stores running HPOS with sycning disabled.
if ( wcs_is_custom_order_tables_usage_enabled() && ! wcs_is_custom_order_tables_data_sync_enabled() ) {
add_action( 'admin_notices', [ __CLASS__, 'display_hpos_incompatibility_notice' ] );
return;
// The subscription reports are compatible with HPOS since 7.8.0.
// We can inform users running data sync mode, that it's no longer needed.
if (
wcs_is_custom_order_tables_usage_enabled() &&
wcs_is_custom_order_tables_data_sync_enabled()
) {
add_action( 'admin_notices', [ __CLASS__, 'display_hpos_compatibility_notice' ] );
}
// Add the reports layout to the WooCommerce -> Reports admin section
@@ -37,12 +40,18 @@ class WCS_Admin_Reports {
// Add any actions we need based on the screen
add_action( 'current_screen', __CLASS__ . '::conditional_reporting_includes' );
// Starting from WooCommerce 10.0 the dashboard widget is loaded asynchronously.
// We also need to hook into AJAX request before WooCommerce so we can attach our hook to widget rendering flow.
add_action( 'wp_ajax_woocommerce_load_status_widget', __CLASS__ . '::init_dashboard_report', 9 );
}
/**
* Displays an admin notice indicating subscription reports are disabled on HPOS environments with no syncing.
* Displays an admin notice indicating subscription reports are compatible with HPOS.
*
* @since 7.8.0
*/
public static function display_hpos_incompatibility_notice() {
public static function display_hpos_compatibility_notice() {
$screen = get_current_screen();
// Only display the admin notice on report admin screens.
@@ -50,21 +59,34 @@ class WCS_Admin_Reports {
return;
}
$admin_notice = new WCS_Admin_Notice( 'error' );
$nonce_name = 'wcs_reports_hpos_compatibility_notice';
$option_name = 'woocommerce_subscriptions_reports_hpos_compatibility_notice_dismissed';
$admin_notice->set_html_content(
sprintf(
'<p><strong>%s</strong></p><p>%s</p>',
_x( 'WooCommerce Subscriptions - Reports Not Available', 'heading used in an admin notice', 'woocommerce-subscriptions' ),
sprintf(
// translators: placeholders $1 and $2 are opening <a> tags linking to the WooCommerce documentation on HPOS, and to the Advanced Feature settings screen. Placeholder $3 is a closing link (<a>) tag.
__( 'Subscription reports are incompatible with the %1$sWooCommerce data storage features%3$s enabled on your store. Please %2$senable compatibility mode%3$s if you wish to use subscription reports.', 'woocommerce-subscriptions' ),
'<a href="https://woocommerce.com/document/high-performance-order-storage/">',
'<a href="' . esc_url( get_admin_url( null, 'admin.php?page=wc-settings&tab=advanced&section=features' ) ) . '">',
'</a>'
)
)
$is_dismissed = get_option( $option_name );
if ( 'yes' === $is_dismissed ) {
return;
}
if ( isset( $_GET['_wcsnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wcsnonce'] ) ), $nonce_name ) && ! empty( $_GET[ $nonce_name ] ) ) {
update_option( $option_name, 'yes' );
return;
}
$dismiss_url = wp_nonce_url( add_query_arg( $nonce_name, '1' ), $nonce_name, '_wcsnonce' );
$admin_notice = new WCS_Admin_Notice( 'notice notice-info is-dismissible', array(), $dismiss_url );
$content = sprintf(
// translators: placeholders $1 and $2 are opening <a> tags linking to the WooCommerce documentation on HPOS, and to the Advanced Features settings screen. Placeholder $3 is a closing link (</a>) tag.
__( 'WooCommerce Subscriptions now supports %1$sHigh-Performance Order Storage (HPOS)%3$s - compatibility mode is no longer required to view subscriptions reports. You can disable compatibility mode in your %2$sstore settings%3$s.', 'woocommerce-subscriptions' ),
'<a href="https://woocommerce.com/document/high-performance-order-storage/">',
'<a href="' . esc_url( get_admin_url( null, 'admin.php?page=wc-settings&tab=advanced&section=features' ) ) . '">',
'</a>'
);
$admin_notice->set_html_content( "<p>{$content}</p>" );
$admin_notice->display();
}
@@ -169,12 +191,19 @@ class WCS_Admin_Reports {
$screen = get_current_screen();
switch ( $screen->id ) {
case 'dashboard':
new WCS_Report_Dashboard();
break;
// Before WooCommerce 10.0 the dashboard widget was loaded synchronously on the dashboard screen. Keep this for backward compatibility.
if ( isset( $screen->id ) && 'dashboard' === $screen->id ) {
self::init_dashboard_report();
}
}
/**
* Initialize the dashboard report.
*
* Used for loading the dashboard widget sync and async.
*/
public static function init_dashboard_report() {
new WCS_Report_Dashboard();
}
/**

View File

@@ -75,15 +75,9 @@ class WCS_Report_Cache_Manager {
/**
* Attach callbacks to manage cache updates
*
* @since 2.1
* @since 7.8.0 - Compatible with HPOS, originally introduced in 2.1
*/
public function __construct() {
// Our reports integration does not work if A) HPOS is enabled and B) compatibility mode is disabled.
// In these cases, there is no reason to cache report data/to update data that was already cached.
if ( wcs_is_custom_order_tables_usage_enabled() && ! wcs_is_custom_order_tables_data_sync_enabled() ) {
return;
}
// Use the old hooks
if ( wcs_is_woocommerce_pre( '3.0' ) ) {
@@ -146,37 +140,38 @@ class WCS_Report_Cache_Manager {
*/
public function schedule_cache_updates() {
if ( ! empty( $this->reports_to_update ) ) {
if ( empty( $this->reports_to_update ) ) {
return;
}
// On large sites, we want to run the cache update once at 4am in the site's timezone
if ( $this->use_large_site_cache() ) {
// On large sites, we want to run the cache update once at 4am in the site's timezone
if ( $this->use_large_site_cache() ) {
$cache_update_timestamp = $this->get_large_site_cache_update_timestamp();
$cache_update_timestamp = $this->get_large_site_cache_update_timestamp();
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
foreach ( $this->reports_to_update as $index => $report_class ) {
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
foreach ( $this->reports_to_update as $index => $report_class ) {
$cron_args = array( 'report_class' => $report_class );
$cron_args = array( 'report_class' => $report_class );
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
as_schedule_single_action( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $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
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
}
} else { // Otherwise, run it 10 minutes after the last cache invalidating event
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
foreach ( $this->reports_to_update as $index => $report_class ) {
// Schedule one update event for each class to avoid updating cache more than once for the same class for different events
foreach ( $this->reports_to_update as $index => $report_class ) {
$cron_args = array( 'report_class' => $report_class );
$cron_args = array( 'report_class' => $report_class );
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
as_schedule_single_action( (int) gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $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
as_schedule_single_action( (int) gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args );
}
}
}

View File

@@ -16,6 +16,20 @@ if ( ! defined( 'ABSPATH' ) ) {
}
class WCS_Report_Dashboard {
/**
* Tracks whether the cache should be updated after generating report data.
*
* @var bool
*/
private static $should_update_cache = false;
/**
* Cached report results for performance optimization.
*
*
* @var array
*/
private static $cached_report_results = array();
/**
* Hook in additional reporting to WooCommerce dashboard widget
@@ -31,181 +45,35 @@ class WCS_Report_Dashboard {
/**
* Get all data needed for this report and store in the class
*
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager to update the cache.
*
* @param array $args The arguments for the report.
* @return object The report data.
*/
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' );
$update_cache = false;
$args = apply_filters( 'wcs_reports_subscription_dashboard_args', $args );
$args = wp_parse_args( $args, $default_args );
// 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 );
self::init_cache();
$report_data = new stdClass;
// Use current month as default date range.
$start_date = $args['start_date'] ?? date( 'Y-m-01', current_time( 'timestamp' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date,WordPress.DateTime.CurrentTimeTimestamp.Requested -- Keep default date values for backward compatibility.
$end_date = $args['end_date'] ?? date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date,WordPress.DateTime.CurrentTimeTimestamp.Requested -- Keep default date values for backward compatibility.
$cached_results = get_transient( strtolower( __CLASS__ ) );
$report_data = new stdClass();
$report_data->signup_count = self::fetch_signup_count( $start_date, $end_date, $args['no_cache'] );
$report_data->signup_revenue = self::fetch_signup_revenue( $start_date, $end_date, $args['no_cache'] );
$report_data->renewal_count = self::fetch_renewal_count( $start_date, $end_date, $args['no_cache'] );
$report_data->renewal_revenue = self::fetch_renewal_revenue( $start_date, $end_date, $args['no_cache'] );
$report_data->cancel_count = self::fetch_cancel_count( $start_date, $end_date, $args['no_cache'] );
// Set a default value for cached results for PHP 8.2+ compatibility.
if ( empty( $cached_results ) ) {
$cached_results = [];
}
// Subscription signups this month
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
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",
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'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) );
$update_cache = true;
}
$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' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query ) );
$update_cache = true;
}
$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
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",
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'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) );
$update_cache = true;
}
$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'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_revenue_query', $query ) );
$update_cache = true;
}
$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', %s ) BETWEEN %s AND %s",
$site_timezone,
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'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_cancellation_query', $query ) );
$update_cache = true;
}
$report_data->cancel_count = $cached_results[ $query_hash ];
if ( $update_cache ) {
set_transient( strtolower( __CLASS__ ), $cached_results, HOUR_IN_SECONDS );
if ( self::$should_update_cache ) {
set_transient( strtolower( __CLASS__ ), self::$cached_report_results, HOUR_IN_SECONDS );
}
return $report_data;
@@ -256,11 +124,11 @@ class WCS_Report_Dashboard {
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date&range=month' ) ); ?>">
<?php
// translators: 1$: count, 2$ and 3$ are opening and closing strong tags, respectively.
echo wp_kses_post( sprintf( _n( '%2$s%1$s cancellation%3$s subscription cancellations this month', '%2$s%1$s cancellations%3$s subscription cancellations this month', $report_data->cancel_count, 'woocommerce-subscriptions' ), $report_data->cancel_count, '<strong>', '</strong>' ) ); ?>
echo wp_kses_post( sprintf( _n( '%2$s%1$s cancellation%3$s subscription cancellations this month', '%2$s%1$s cancellations%3$s subscription cancellations this month', $report_data->cancel_count, 'woocommerce-subscriptions' ), $report_data->cancel_count, '<strong>', '</strong>' ) );
?>
</a>
</li>
<?php
}
/**
@@ -275,9 +143,374 @@ class WCS_Report_Dashboard {
/**
* Clears the cached report data.
*
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager before updating the cache.
*
* @since 3.0.10
*/
public static function clear_cache() {
delete_transient( strtolower( __CLASS__ ) );
self::$should_update_cache = false;
self::$cached_report_results = array();
}
/**
* Fetch the signup count for the dashboard.
*
* @param string $start_date The start date.
* @param string $end_date The end date.
* @param bool $force_cache_update Whether to force update the cache.
* @return int The signup count.
*/
private static function fetch_signup_count( $start_date, $end_date, $force_cache_update = false ) {
global $wpdb;
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
FROM {$wpdb->prefix}wc_orders AS wcsubs
INNER JOIN {$wpdb->prefix}wc_orders AS wcorder
ON wcsubs.parent_order_id = wcorder.ID
WHERE wcorder.type IN ( 'shop_order' )
AND wcsubs.type IN ( 'shop_subscription' )
AND wcorder.status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.date_created_gmt >= %s
AND wcorder.date_created_gmt < %s",
$start_date,
$end_date
);
} else {
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
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",
$start_date,
$end_date
);
}
$query_hash = md5( $query );
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
/**
* Filter the query for the signup count.
*
* @param string $query The query to execute.
* @return string The filtered query.
*
* @since 3.0.10
*/
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query );
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
self::cache_report_results( $query_hash, $query_results );
}
return self::$cached_report_results[ $query_hash ];
}
/**
* Fetch the signup revenue for the dashboard.
*
* @param string $start_date The start date.
* @param string $end_date The end date.
* @param bool $force_cache_update Whether to force update the cache.
* @return float The signup revenue.
*/
private static function fetch_signup_revenue( $start_date, $end_date, $force_cache_update = false ) {
global $wpdb;
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT SUM(parent_orders.total_amount)
FROM {$wpdb->prefix}wc_orders AS subscripitons
INNER JOIN {$wpdb->prefix}wc_orders AS parent_orders
ON subscripitons.parent_order_id = parent_orders.ID
WHERE parent_orders.type IN ( 'shop_order' )
AND subscripitons.type IN ( 'shop_subscription' )
AND parent_orders.status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND parent_orders.date_created_gmt >= %s
AND parent_orders.date_created_gmt < %s
",
$start_date,
$end_date
);
} else {
$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'",
$start_date,
$end_date
);
}
$query_hash = md5( $query );
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
/**
* Filter the query for the signup revenue.
*
* @param string $query The query to execute.
* @return string The filtered query.
*
* @since 3.0.10
*/
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query );
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
self::cache_report_results( $query_hash, $query_results );
}
return self::$cached_report_results[ $query_hash ];
}
/**
* Fetch the renewal count for the dashboard.
*
* @param string $start_date The start date.
* @param string $end_date The end date.
* @param bool $force_cache_update Whether to force update the cache.
* @return int The renewal count.
*/
private static function fetch_renewal_count( $start_date, $end_date, $force_cache_update = false ) {
global $wpdb;
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcorder.ID) AS count
FROM {$wpdb->prefix}wc_orders AS wcorder
INNER JOIN {$wpdb->prefix}wc_orders_meta AS meta__subscription_renewal
ON (
wcorder.id = meta__subscription_renewal.order_id
AND
meta__subscription_renewal.meta_key = '_subscription_renewal'
)
WHERE wcorder.type IN ( 'shop_order' )
AND wcorder.status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.date_created_gmt >= %s
AND wcorder.date_created_gmt < %s",
$start_date,
$end_date
);
} else {
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcorder.ID) AS count
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",
$start_date,
$end_date
);
}
$query_hash = md5( $query );
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
/**
* Filter the query for the renewal count.
*
* @param string $query The query to execute.
* @return string The filtered query.
*
* @since 3.0.10
*/
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query );
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
self::cache_report_results( $query_hash, $query_results );
}
return self::$cached_report_results[ $query_hash ];
}
/**
* Fetch the renewal revenue for the dashboard.
*
* @param string $start_date The start date.
* @param string $end_date The end date.
* @param bool $force_cache_update Whether to force update the cache.
* @return float The renewal revenue.
*/
private static function fetch_renewal_revenue( $start_date, $end_date, $force_cache_update = false ) {
global $wpdb;
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT SUM(wcorder.total_amount)
FROM {$wpdb->prefix}wc_orders AS wcorder
INNER JOIN {$wpdb->prefix}wc_orders_meta AS meta__subscription_renewal
ON (
wcorder.id = meta__subscription_renewal.order_id
AND
meta__subscription_renewal.meta_key = '_subscription_renewal'
)
WHERE wcorder.type IN ( 'shop_order' )
AND wcorder.status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.date_created_gmt >= %s
AND wcorder.date_created_gmt < %s",
$start_date,
$end_date
);
} else {
$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'",
$start_date,
$end_date
);
}
$query_hash = md5( $query );
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
/**
* Filter the query for the renewal revenue.
*
* @param string $query The query to execute.
* @return string The filtered query.
*
* @since 3.0.10
*/
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_revenue_query', $query );
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
self::cache_report_results( $query_hash, $query_results );
}
return self::$cached_report_results[ $query_hash ];
}
/**
* Fetch the cancellation count for the dashboard.
*
* @param string $start_date The start date.
* @param string $end_date The end date.
* @param bool $force_cache_update Whether to force update the cache.
* @return int The cancellation count.
*/
private static function fetch_cancel_count( $start_date, $end_date, $force_cache_update = false ) {
global $wpdb;
$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 );
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
FROM {$wpdb->prefix}wc_orders AS wcsubs
JOIN {$wpdb->prefix}wc_orders_meta AS wcsmeta_cancel
ON wcsubs.ID = wcsmeta_cancel.order_id
AND wcsmeta_cancel.meta_key = '_schedule_cancelled'
AND wcsubs.status NOT IN ( 'trash', 'auto-draft' )
AND CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', %s ) BETWEEN %s AND %s",
$site_timezone,
$start_date,
$end_date
);
} else {
$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', %s ) BETWEEN %s AND %s",
$site_timezone,
$start_date,
$end_date
);
}
$query_hash = md5( $query );
if ( $force_cache_update || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
/**
* Filter the query for the cancellation count.
*
* @param string $query The query to execute.
* @return string The filtered query.
*
* @since 3.0.10
*/
$query = apply_filters( 'woocommerce_subscription_dashboard_status_widget_cancellation_query', $query );
$query_results = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
self::cache_report_results( $query_hash, $query_results );
}
return self::$cached_report_results[ $query_hash ];
}
/**
* Initialize cache for report results.
*
* @return void
*/
private static function init_cache() {
self::$should_update_cache = false;
self::$cached_report_results = get_transient( strtolower( __CLASS__ ) );
// Set a default value for cached results for PHP 8.2+ compatibility.
if ( empty( self::$cached_report_results ) ) {
self::$cached_report_results = array();
}
}
/**
* Cache report results for performance optimization.
*
* @param string $query_hash The hash of the query for caching.
* @param array $report_data The report data to cache.
* @return void
*/
private static function cache_report_results( $query_hash, $report_data ) {
self::$cached_report_results[ $query_hash ] = $report_data;
self::$should_update_cache = true;
}
}

View File

@@ -21,7 +21,7 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
* Get report data
*
* @since 2.1
* @return array
* @return stdClass
*/
public function get_report_data() {
if ( empty( $this->report_data ) ) {
@@ -41,22 +41,10 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
* @return void
*/
private function query_report_data() {
global $wpdb;
$this->report_data = new stdClass;
// First, let's find the age of the longest living subscription in days
$oldest_subscription_age_in_days = $wpdb->get_var( $wpdb->prepare(
"SELECT MAX(DATEDIFF(CAST(postmeta.meta_value AS DATETIME),posts.post_date_gmt)) as age_in_days
FROM {$wpdb->prefix}posts posts
LEFT JOIN {$wpdb->prefix}postmeta postmeta ON posts.ID = postmeta.post_id
WHERE posts.post_type = 'shop_subscription'
AND postmeta.meta_key = %s
AND postmeta.meta_value <> '0'
ORDER BY age_in_days DESC
LIMIT 1",
wcs_get_date_meta_key( 'end' )
) );
$oldest_subscription_age_in_days = $this->get_max_subscription_age_in_days();
// Now determine what interval to use based on that length
if ( $oldest_subscription_age_in_days > 365 ) {
@@ -74,34 +62,12 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
$oldest_subscription_age = floor( $oldest_subscription_age_in_days / $days_in_interval_period );
// Now get all subscriptions, not just those that have ended, and find out how long they have lived (or if they haven't ended yet, consider them as being alive for one period longer than the longest living subsription)
$subscription_ages = $wpdb->get_results(
$wpdb->prepare(
"SELECT
IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),posts.post_date_gmt)/%d),%d) as periods_active,
COUNT(posts.ID) as count
FROM {$wpdb->prefix}posts posts
LEFT JOIN {$wpdb->prefix}postmeta cancelled_date
ON posts.ID = cancelled_date.post_id
AND cancelled_date.meta_key = %s
AND cancelled_date.meta_value <> '0'
LEFT JOIN {$wpdb->prefix}postmeta end_date
ON posts.ID = end_date.post_id
AND end_date.meta_key = %s
WHERE posts.post_type = 'shop_subscription'
AND posts.post_status NOT IN( 'wc-pending', 'trash' )
GROUP BY periods_active
ORDER BY periods_active ASC",
$days_in_interval_period,
( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subscription
wcs_get_date_meta_key( 'cancelled' ), // If a subscription has a cancelled date, use that to determine a more accurate lifetime
wcs_get_date_meta_key( 'end' ) // Otherwise, we want to use the end date for subscriptions that have expired
),
OBJECT_K
);
$subscription_ages = $this->fetch_subscriptions_ages( $days_in_interval_period, $oldest_subscription_age );
$this->report_data->total_subscriptions = $this->report_data->unended_subscriptions = absint( array_sum( wp_list_pluck( $subscription_ages, 'count' ) ) );
$this->report_data->living_subscriptions = array();
// Set initial values for the report data.
$this->report_data->total_subscriptions = absint( array_sum( wp_list_pluck( $subscription_ages, 'count' ) ) );
$this->report_data->unended_subscriptions = $this->report_data->total_subscriptions;
$this->report_data->living_subscriptions = array();
// At day zero, no subscriptions have ended
$this->report_data->living_subscriptions[0] = $this->report_data->total_subscriptions;
@@ -122,6 +88,107 @@ class WCS_Report_Retention_Rate extends WC_Admin_Report {
}
}
/**
* Get the age of the longest living subscription in days.
*
* @return int
*/
private function get_max_subscription_age_in_days() {
global $wpdb;
$end_date_meta_key = wcs_get_date_meta_key( 'end' );
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT MAX(DATEDIFF(CAST(meta.meta_value AS DATETIME),orders.date_created_gmt)) as age_in_days
FROM {$wpdb->prefix}wc_orders orders
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta ON orders.ID = meta.order_id
WHERE orders.type = 'shop_subscription'
AND meta.meta_key = %s
AND meta.meta_value <> '0'
ORDER BY age_in_days DESC
LIMIT 1",
$end_date_meta_key
);
} else {
$query = $wpdb->prepare(
"SELECT MAX(DATEDIFF(CAST(postmeta.meta_value AS DATETIME),posts.post_date_gmt)) as age_in_days
FROM {$wpdb->prefix}posts posts
LEFT JOIN {$wpdb->prefix}postmeta postmeta ON posts.ID = postmeta.post_id
WHERE posts.post_type = 'shop_subscription'
AND postmeta.meta_key = %s
AND postmeta.meta_value <> '0'
ORDER BY age_in_days DESC
LIMIT 1",
$end_date_meta_key
);
}
return $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The query is prepared above.
}
/**
* Fetch the number of periods each subscription has between creating and ending.
*
* @param int $days_in_interval_period
* @param int $oldest_subscription_age
* @return array
*/
private function fetch_subscriptions_ages( $days_in_interval_period, $oldest_subscription_age ) {
global $wpdb;
$end_date_meta_key = wcs_get_date_meta_key( 'end' );
$cancelled_date_meta_key = wcs_get_date_meta_key( 'cancelled' );
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT
IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),orders.date_created_gmt)/%d),%d) as periods_active,
COUNT(orders.ID) as count
FROM {$wpdb->prefix}wc_orders orders
LEFT JOIN {$wpdb->prefix}wc_orders_meta cancelled_date
ON orders.ID = cancelled_date.order_id
AND cancelled_date.meta_key = %s
AND cancelled_date.meta_value <> '0'
LEFT JOIN {$wpdb->prefix}wc_orders_meta end_date
ON orders.ID = end_date.order_id
AND end_date.meta_key = %s
WHERE orders.type = 'shop_subscription'
AND orders.status NOT IN( 'wc-pending', 'trash' )
GROUP BY periods_active
ORDER BY periods_active ASC",
$days_in_interval_period,
( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subscription
$cancelled_date_meta_key, // If a subscription has a cancelled date, use that to determine a more accurate lifetime
$end_date_meta_key // Otherwise, we want to use the end date for subscriptions that have expired
);
} else {
$query = $wpdb->prepare(
"SELECT
IF(COALESCE(cancelled_date.meta_value,end_date.meta_value) <> '0',CEIL(DATEDIFF(CAST(COALESCE(cancelled_date.meta_value,end_date.meta_value) AS DATETIME),posts.post_date_gmt)/%d),%d) as periods_active,
COUNT(posts.ID) as count
FROM {$wpdb->prefix}posts posts
LEFT JOIN {$wpdb->prefix}postmeta cancelled_date
ON posts.ID = cancelled_date.post_id
AND cancelled_date.meta_key = %s
AND cancelled_date.meta_value <> '0'
LEFT JOIN {$wpdb->prefix}postmeta end_date
ON posts.ID = end_date.post_id
AND end_date.meta_key = %s
WHERE posts.post_type = 'shop_subscription'
AND posts.post_status NOT IN( 'wc-pending', 'trash' )
GROUP BY periods_active
ORDER BY periods_active ASC",
$days_in_interval_period,
( $oldest_subscription_age + 1 ), // Consider living subscriptions as being alive for one period longer than the longest living subscription
$cancelled_date_meta_key, // If a subscription has a cancelled date, use that to determine a more accurate lifetime
$end_date_meta_key // Otherwise, we want to use the end date for subscriptions that have expired
);
}
return $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The query is prepared above.
}
/**
* Output the report
*

View File

@@ -11,6 +11,12 @@
* @since 2.1
*/
class WCS_Report_Subscription_By_Customer extends WP_List_Table {
/**
* Cached report results.
*
* @var array
*/
private static $cached_report_results = array();
private $totals;
@@ -25,6 +31,15 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
) );
}
/**
* Get the totals.
*
* @return object
*/
public function get_totals() {
return $this->totals;
}
/**
* No subscription products found text.
*/
@@ -112,8 +127,6 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
* Prepare subscription list items.
*/
public function prepare_items() {
global $wpdb;
$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
$current_page = absint( $this->get_pagenum() );
$per_page = absint( apply_filters( 'wcs_reports_customers_per_page', 20 ) );
@@ -123,130 +136,148 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
$active_statuses = wcs_maybe_prefix_key( apply_filters( 'wcs_reports_active_statuses', [ 'active', 'pending-cancel' ] ), 'wc-' );
$paid_statuses = wcs_maybe_prefix_key( apply_filters( 'woocommerce_reports_paid_order_statuses', [ 'completed', 'processing' ] ), 'wc-' );
$active_statuses_placeholders = implode( ',', array_fill( 0, count( $active_statuses ), '%s' ) );
$paid_statuses_placeholders = implode( ',', array_fill( 0, count( $paid_statuses ), '%s' ) );
// Ignored for allowing interpolation in the IN statements.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
$query = apply_filters( 'wcs_reports_current_customer_query',
$wpdb->prepare(
"SELECT customer_ids.meta_value as customer_id,
COUNT(subscription_posts.ID) as total_subscriptions,
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
COUNT(DISTINCT parent_order.ID) as initial_order_count,
SUM(CASE
WHEN subscription_posts.post_status
IN ( {$active_statuses_placeholders} ) THEN 1
ELSE 0
END) AS active_subscriptions
FROM {$wpdb->posts} subscription_posts
INNER JOIN {$wpdb->postmeta} customer_ids
ON customer_ids.post_id = subscription_posts.ID
AND customer_ids.meta_key = '_customer_user'
LEFT JOIN {$wpdb->posts} parent_order
ON parent_order.ID = subscription_posts.post_parent
AND parent_order.post_status IN ( {$paid_statuses_placeholders} )
LEFT JOIN {$wpdb->postmeta} parent_total
ON parent_total.post_id = parent_order.ID
AND parent_total.meta_key = '_order_total'
WHERE subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
GROUP BY customer_ids.meta_value
ORDER BY customer_id DESC
LIMIT %d, %d",
array_merge( $active_statuses, $paid_statuses, [ $offset, $per_page ] )
)
$query_options = array(
'active_statuses' => $active_statuses,
'paid_statuses' => $paid_statuses,
'offset' => $offset,
'per_page' => $per_page,
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$this->items = $wpdb->get_results( $query );
$this->items = self::fetch_subscriptions_by_customer( $query_options );
$customer_ids = wp_list_pluck( $this->items, 'customer_id' );
$customer_ids = wp_list_pluck( $this->items, 'customer_id' );
$customer_placeholders = implode( ',', array_fill( 0, count( $customer_ids ), '%s' ) );
$paid_statuses = wcs_maybe_prefix_key( apply_filters( 'woocommerce_reports_paid_order_statuses', [ 'completed', 'processing' ] ), 'wc-' );
$status_placeholders = implode( ',', array_fill( 0, count( $paid_statuses ), '%s' ) );
// Now get each customer's renewal and switch total
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Ignored for allowing interpolation in the IN statements.
$customer_renewal_switch_total_query = apply_filters( 'wcs_reports_current_customer_renewal_switch_total_query',
$wpdb->prepare(
"SELECT
customer_ids.meta_value as customer_id,
COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
FROM {$wpdb->postmeta} renewal_order_ids
INNER JOIN {$wpdb->posts} subscription_posts
ON renewal_order_ids.meta_value = subscription_posts.ID
AND subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
INNER JOIN {$wpdb->postmeta} customer_ids
ON renewal_order_ids.meta_value = customer_ids.post_id
AND customer_ids.meta_key = '_customer_user'
AND customer_ids.meta_value IN ( {$customer_placeholders} )
INNER JOIN {$wpdb->posts} renewal_order_posts
ON renewal_order_ids.post_id = renewal_order_posts.ID
AND renewal_order_posts.post_status IN ( {$status_placeholders} )
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
AND renewal_switch_totals.meta_key = '_order_total'
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch'
GROUP BY customer_id
ORDER BY customer_id",
array_merge( $customer_ids, $paid_statuses )
)
$related_orders_query_options = array(
'order_status' => $paid_statuses,
'customer_ids' => $customer_ids,
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare.
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$customer_renewal_switch_totals = $wpdb->get_results( $customer_renewal_switch_total_query, OBJECT_K );
$related_orders_totals_by_customer = self::fetch_subscriptions_related_orders_totals_by_customer( $related_orders_query_options );
foreach ( $this->items as $index => $item ) {
if ( isset( $customer_renewal_switch_totals[ $item->customer_id ] ) ) {
$this->items[ $index ]->renewal_switch_total = $customer_renewal_switch_totals[ $item->customer_id ]->renewal_switch_total;
$this->items[ $index ]->renewal_switch_count = $customer_renewal_switch_totals[ $item->customer_id ]->renewal_switch_count;
if ( isset( $related_orders_totals_by_customer[ $item->customer_id ] ) ) {
$this->items[ $index ]->renewal_switch_total = $related_orders_totals_by_customer[ $item->customer_id ]->renewal_switch_total;
$this->items[ $index ]->renewal_switch_count = $related_orders_totals_by_customer[ $item->customer_id ]->renewal_switch_count;
} else {
$this->items[ $index ]->renewal_switch_total = $this->items[ $index ]->renewal_switch_count = 0;
$this->items[ $index ]->renewal_switch_total = 0;
$this->items[ $index ]->renewal_switch_count = 0;
}
}
/**
* Pagination.
*/
$this->set_pagination_args( array(
'total_items' => $this->totals->total_customers,
'per_page' => $per_page,
'total_pages' => ceil( $this->totals->total_customers / $per_page ),
) );
$this->set_pagination_args(
array(
'total_items' => $this->totals->total_customers,
'per_page' => $per_page,
'total_pages' => ceil( $this->totals->total_customers / $per_page ),
)
);
}
/**
* Gather totals for customers
*/
* Gather totals for customers.
*
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager to update the cache.
*
* @param array $args The arguments for the report.
* @return object The totals for customers.
*/
public static function get_data( $args = array() ) {
global $wpdb;
$default_args = array(
'no_cache' => false,
/**
* Filter the order statuses considered as "paid" for the report.
*
* @param array $order_statuses The default paid order statuses: completed, processing.
* @return array The filtered order statuses.
*
* @since 2.1.0
*/
'order_status' => apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ),
);
/**
* Filter the arguments for the totals of subscriptions by customer report.
*
* @param array $args The arguments for the report.
* @return array The filtered arguments.
*
* @since 2.1.0
*/
$args = apply_filters( 'wcs_reports_customer_total_args', $args );
$args = wp_parse_args( $args, $default_args );
self::init_cache();
$subscriptions_totals = self::fetch_customer_subscription_totals( $args );
$related_orders_totals = self::fetch_customer_subscription_related_orders_totals( $args );
$subscriptions_totals->renewal_switch_total = $related_orders_totals->renewal_switch_total;
$subscriptions_totals->renewal_switch_count = $related_orders_totals->renewal_switch_count;
return $subscriptions_totals;
}
/**
* Clears the cached report data.
*
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager before updating the cache.
*
* @since 3.0.10
*/
public static function clear_cache() {
delete_transient( strtolower( __CLASS__ ) );
self::$cached_report_results = array();
}
/**
* Fetch totals by customer for subscriptions.
*
* @param array $args The arguments for the report.
* @return object The totals by customer for subscriptions.
*
* @since 2.1.0
*/
public static function fetch_customer_subscription_totals( $args = array() ) {
global $wpdb;
/**
* Filter the active subscription statuses used for reporting.
*
* @param array $active_statuses The default active subscription statuses: active, pending-cancel.
* @return array The filtered active statuses.
*
* @since 2.1.0
*/
$active_statuses = wcs_maybe_prefix_key( apply_filters( 'wcs_reports_active_statuses', [ 'active', 'pending-cancel' ] ), 'wc-' );
$order_statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
$active_statuses_placeholders = implode( ',', array_fill( 0, count( $active_statuses ), '%s' ) );
$order_statuses_placeholders = implode( ',', array_fill( 0, count( $order_statuses ), '%s' ) );
$total_query = apply_filters( 'wcs_reports_customer_total_query',
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
$wpdb->prepare(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT COUNT( DISTINCT subscriptions.customer_id) as total_customers,
COUNT(subscriptions.ID) as total_subscriptions,
COALESCE( SUM(parent_orders.total_amount), 0) as initial_order_total,
COUNT(DISTINCT parent_orders.ID) as initial_order_count,
COALESCE(SUM(CASE
WHEN subscriptions.status
IN ( {$active_statuses_placeholders} ) THEN 1
ELSE 0
END), 0) AS active_subscriptions
FROM {$wpdb->prefix}wc_orders subscriptions
LEFT JOIN {$wpdb->prefix}wc_orders parent_orders
ON parent_orders.ID = subscriptions.parent_order_id
AND parent_orders.status IN ( {$order_statuses_placeholders} )
WHERE subscriptions.type = 'shop_subscription'
AND subscriptions.status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders are prepared above.
array_merge( $active_statuses, $order_statuses )
);
} else {
$query = $wpdb->prepare(
"SELECT COUNT( DISTINCT customer_ids.meta_value) as total_customers,
COUNT(subscription_posts.ID) as total_subscriptions,
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
@@ -267,43 +298,86 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
ON parent_total.post_id = parent_order.ID
AND parent_total.meta_key = '_order_total'
WHERE subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
",
array_merge( $active_statuses, $order_statuses )
) );
AND subscription_posts.post_status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders are prepared above.
array_merge( $active_statuses, $order_statuses )
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared.
$cached_results = get_transient( strtolower( __CLASS__ ) );
$query_hash = md5( $total_query );
/**
* Filter the query used to fetch the customer subscription totals.
*
* @param string $query The query to fetch the customer subscription totals.
* @return string The filtered query.
*
* @since 2.1.0
*/
$query = apply_filters( 'wcs_reports_customer_total_query', $query );
$query_hash = md5( $query );
// Set a default value for cached results for PHP 8.2+ compatibility.
if ( empty( $cached_results ) ) {
$cached_results = [];
}
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
// Enable big selects for reports
// We expect that cache was initialized before calling this method.
// Skip running the query if cache is available.
if ( $args['no_cache'] || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_data', $wpdb->get_row( $total_query ) );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
$query_results = $wpdb->get_row( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
/**
* Filter the query results for customer totals.
*
* @param object $query_results The query results.
* @return object The filtered query results.
*
* @since 2.1.0
*/
$query_results = apply_filters( 'wcs_reports_customer_total_data', $query_results );
self::cache_report_results( $query_hash, $query_results );
}
$customer_totals = $cached_results[ $query_hash ];
return self::$cached_report_results[ $query_hash ];
}
/**
* Fetch totals by customer for related renewal and switch orders.
*
* @param array $args The arguments for the report.
* @return object The totals by customer for related renewal and switch orders.
*
* @since 2.1.0
*/
public static function fetch_customer_subscription_related_orders_totals( $args = array() ) {
global $wpdb;
$status_placeholders = implode( ',', array_fill( 0, count( $args['order_status'] ), '%s' ) );
$statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
$renewal_switch_total_query = apply_filters( 'wcs_reports_customer_total_renewal_switch_query',
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
$wpdb->prepare(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in the IN statements.
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT COALESCE( SUM(renewal_orders.total_amount), 0) as renewal_switch_total,
COUNT(DISTINCT renewal_orders.ID) as renewal_switch_count
FROM {$wpdb->prefix}wc_orders_meta renewal_order_ids
INNER JOIN {$wpdb->prefix}wc_orders subscriptions
ON renewal_order_ids.meta_value = subscriptions.ID
AND subscriptions.type = 'shop_subscription'
AND subscriptions.status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
INNER JOIN {$wpdb->prefix}wc_orders renewal_orders
ON renewal_order_ids.order_id = renewal_orders.ID
AND renewal_orders.status IN ( {$status_placeholders} )
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch'
", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders are prepared above.
$statuses
);
} else {
$query = $wpdb->prepare(
"SELECT COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
FROM {$wpdb->postmeta} renewal_order_ids
INNER JOIN {$wpdb->posts} subscription_posts
FROM {$wpdb->postmeta} renewal_order_ids
INNER JOIN {$wpdb->posts} subscription_posts
ON renewal_order_ids.meta_value = subscription_posts.ID
AND subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'trash')
AND subscription_posts.post_status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
INNER JOIN {$wpdb->posts} renewal_order_posts
ON renewal_order_ids.post_id = renewal_order_posts.ID
AND renewal_order_posts.post_status IN ( {$status_placeholders} )
@@ -311,34 +385,238 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
AND renewal_switch_totals.meta_key = '_order_total'
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch'",
OR renewal_order_ids.meta_key = '_subscription_switch'
", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders are prepared above.
$statuses
)
);
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared.
$query_hash = md5( $renewal_switch_total_query );
/**
* Filter the query used to fetch the customer subscription related orders totals.
*
* @param string $query The query to fetch the customer subscription related orders totals.
* @return string The filtered query.
*
* @since 2.1.0
*/
$query = apply_filters( 'wcs_reports_customer_total_renewal_switch_query', $query );
$query_hash = md5( $query );
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
if ( $args['no_cache'] || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
// Enable big selects for reports
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_customer_total_renewal_switch_data', $wpdb->get_row( $renewal_switch_total_query ) );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
$query_results = $wpdb->get_row( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
/**
* Filter the query results for customer subscription related orders totals.
*
* @param object $query_results The query results.
* @return object The filtered query results.
*
* @since 2.1.0
*/
$query_results = apply_filters( 'wcs_reports_customer_total_renewal_switch_data', $query_results );
self::cache_report_results( $query_hash, $query_results );
}
$customer_totals->renewal_switch_total = $cached_results[ $query_hash ]->renewal_switch_total;
$customer_totals->renewal_switch_count = $cached_results[ $query_hash ]->renewal_switch_count;
return $customer_totals;
return self::$cached_report_results[ $query_hash ];
}
/**
* Clears the cached report data.
* Fetch subscriptions by customer.
*
* @since 3.0.10
* @param array $query_options The query options.
* @return array The subscriptions by customer.
*
* @since 2.1.0
*/
public static function clear_cache() {
delete_transient( strtolower( __CLASS__ ) );
private static function fetch_subscriptions_by_customer( $query_options = array() ) {
global $wpdb;
$active_statuses = $query_options['active_statuses'] ?? array();
$paid_statuses = $query_options['paid_statuses'] ?? array();
$offset = $query_options['offset'] ?? 0;
$per_page = $query_options['per_page'] ?? 20;
$active_statuses_placeholders = implode( ',', array_fill( 0, count( $active_statuses ), '%s' ) );
$paid_statuses_placeholders = implode( ',', array_fill( 0, count( $paid_statuses ), '%s' ) );
// Ignored for allowing interpolation in the IN statements.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT subscriptions.customer_id as customer_id,
COUNT(subscriptions.ID) as total_subscriptions,
COALESCE( SUM(parent_order.total_amount), 0) as initial_order_total,
COUNT(DISTINCT parent_order.ID) as initial_order_count,
SUM(CASE
WHEN subscriptions.status
IN ( {$active_statuses_placeholders} ) THEN 1
ELSE 0
END) AS active_subscriptions
FROM {$wpdb->prefix}wc_orders subscriptions
LEFT JOIN {$wpdb->prefix}wc_orders parent_order
ON parent_order.ID = subscriptions.parent_order_id
AND parent_order.status IN ( {$paid_statuses_placeholders} )
WHERE subscriptions.type = 'shop_subscription'
AND subscriptions.status NOT IN ('wc-pending','auto-draft', 'wc-checkout-draft', 'trash')
GROUP BY subscriptions.customer_id
ORDER BY customer_id DESC
LIMIT %d, %d
",
array_merge( $active_statuses, $paid_statuses, array( $offset, $per_page ) )
);
} else {
$query = $wpdb->prepare(
"SELECT customer_ids.meta_value as customer_id,
COUNT(subscription_posts.ID) as total_subscriptions,
COALESCE( SUM(parent_total.meta_value), 0) as initial_order_total,
COUNT(DISTINCT parent_order.ID) as initial_order_count,
SUM(CASE
WHEN subscription_posts.post_status
IN ( {$active_statuses_placeholders} ) THEN 1
ELSE 0
END) AS active_subscriptions
FROM {$wpdb->posts} subscription_posts
INNER JOIN {$wpdb->postmeta} customer_ids
ON customer_ids.post_id = subscription_posts.ID
AND customer_ids.meta_key = '_customer_user'
LEFT JOIN {$wpdb->posts} parent_order
ON parent_order.ID = subscription_posts.post_parent
AND parent_order.post_status IN ( {$paid_statuses_placeholders} )
LEFT JOIN {$wpdb->postmeta} parent_total
ON parent_total.post_id = parent_order.ID
AND parent_total.meta_key = '_order_total'
WHERE subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
GROUP BY customer_ids.meta_value
ORDER BY customer_id DESC
LIMIT %d, %d
",
array_merge( $active_statuses, $paid_statuses, array( $offset, $per_page ) )
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
/**
* Filter the query used to fetch the subscriptions by customer.
*
* @param string $query The query to fetch the subscriptions by customer.
* @return string The filtered query.
*
* @since 2.1.0
*/
$query = apply_filters( 'wcs_reports_current_customer_query', $query );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
return $wpdb->get_results( $query );
}
/**
* Fetch totals by customer for related renewal and switch orders.
*
* @param array $query_options The query options.
* @return array The totals by customer for related renewal and switch orders.
*
* @since 2.1.0
*/
private static function fetch_subscriptions_related_orders_totals_by_customer( $query_options = array() ) {
global $wpdb;
$paid_statuses = $query_options['order_status'] ?? array();
$customer_ids = $query_options['customer_ids'] ?? array();
$customer_placeholders = implode( ',', array_fill( 0, count( $customer_ids ), '%s' ) );
$status_placeholders = implode( ',', array_fill( 0, count( $paid_statuses ), '%s' ) );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Ignored for allowing interpolation in the IN statements.
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT
renewal_orders.customer_id as customer_id,
COALESCE( SUM(renewal_orders.total_amount), 0) as renewal_switch_total,
COUNT(DISTINCT renewal_orders.ID) as renewal_switch_count
FROM {$wpdb->prefix}wc_orders_meta renewal_order_ids
INNER JOIN {$wpdb->prefix}wc_orders subscriptions
ON renewal_order_ids.meta_value = subscriptions.ID
AND subscriptions.type = 'shop_subscription'
AND subscriptions.status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
INNER JOIN {$wpdb->prefix}wc_orders renewal_orders
ON renewal_order_ids.order_id = renewal_orders.ID
AND renewal_orders.status IN ( {$status_placeholders} )
AND renewal_orders.customer_id IN ( {$customer_placeholders} )
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch'
GROUP BY renewal_orders.customer_id
ORDER BY renewal_orders.customer_id
",
array_merge( $paid_statuses, $customer_ids )
);
} else {
$query = $wpdb->prepare(
"SELECT
customer_ids.meta_value as customer_id,
COALESCE( SUM(renewal_switch_totals.meta_value), 0) as renewal_switch_total,
COUNT(DISTINCT renewal_order_posts.ID) as renewal_switch_count
FROM {$wpdb->postmeta} renewal_order_ids
INNER JOIN {$wpdb->posts} subscription_posts
ON renewal_order_ids.meta_value = subscription_posts.ID
AND subscription_posts.post_type = 'shop_subscription'
AND subscription_posts.post_status NOT IN ('wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash')
INNER JOIN {$wpdb->postmeta} customer_ids
ON renewal_order_ids.meta_value = customer_ids.post_id
AND customer_ids.meta_key = '_customer_user'
AND customer_ids.meta_value IN ( {$customer_placeholders} )
INNER JOIN {$wpdb->posts} renewal_order_posts
ON renewal_order_ids.post_id = renewal_order_posts.ID
AND renewal_order_posts.post_status IN ( {$status_placeholders} )
LEFT JOIN {$wpdb->postmeta} renewal_switch_totals
ON renewal_switch_totals.post_id = renewal_order_ids.post_id
AND renewal_switch_totals.meta_key = '_order_total'
WHERE renewal_order_ids.meta_key = '_subscription_renewal'
OR renewal_order_ids.meta_key = '_subscription_switch'
GROUP BY customer_id
ORDER BY customer_id
",
array_merge( $customer_ids, $paid_statuses )
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare.
/**
* Filter the query used to fetch the totals by customer for related renewal and switch orders.
*
* @param string $query The query to fetch the totals by customer for related renewal and switch orders.
* @return string The filtered query.
*
* @since 2.1.0
*/
$query = apply_filters( 'wcs_reports_current_customer_renewal_switch_total_query', $query );
return $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
}
/**
* Initialize cache for report results.
*/
private static function init_cache() {
self::$cached_report_results = get_transient( strtolower( __CLASS__ ) );
// Set a default value for cached results for PHP 8.2+ compatibility.
if ( empty( self::$cached_report_results ) ) {
self::$cached_report_results = array();
}
}
/**
* Cache report results.
*
* @param string $query_hash The query hash.
* @param array $report_data The report data.
*/
private static function cache_report_results( $query_hash, $report_data ) {
self::$cached_report_results[ $query_hash ] = $report_data;
set_transient( strtolower( __CLASS__ ), self::$cached_report_results, WEEK_IN_SECONDS );
}
}

View File

@@ -12,15 +12,24 @@
*/
class WCS_Report_Subscription_By_Product extends WP_List_Table {
/**
* Cached report results.
*
* @var array
*/
private static $cached_report_results = array();
/**
* Constructor.
*/
public function __construct() {
parent::__construct( array(
'singular' => __( 'Product', 'woocommerce-subscriptions' ),
'plural' => __( 'Products', 'woocommerce-subscriptions' ),
'ajax' => false,
) );
parent::__construct(
array(
'singular' => __( 'Product', 'woocommerce-subscriptions' ),
'plural' => __( 'Products', 'woocommerce-subscriptions' ),
'ajax' => false,
)
);
}
/**
@@ -108,137 +117,37 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
/**
* Get subscription product data, either from the cache or the database.
*
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager to update the cache.
*
* @param array $args The arguments for the report.
* @return array The subscription product data.
*/
public static function get_data( $args = array() ) {
global $wpdb;
$default_args = array(
'no_cache' => false,
'order_status' => apply_filters( 'woocommerce_reports_paid_order_statuses', array( 'completed', 'processing' ) ),
);
/**
* Filter the arguments for the subscription by product report.
*
* @param array $args The arguments for the report.
* @return array The filtered arguments.
*
* @since 2.1.0
*/
$args = apply_filters( 'wcs_reports_product_args', $args );
$args = wp_parse_args( $args, $default_args );
$query = apply_filters( 'wcs_reports_product_query',
"SELECT product.id as product_id,
product.post_parent as parent_product_id,
product.post_title as product_name,
mo.product_type,
COUNT(subscription_line_items.subscription_id) as subscription_count,
SUM(subscription_line_items.product_total) as recurring_total
FROM {$wpdb->posts} AS product
LEFT JOIN (
SELECT tr.object_id AS product_id, t.slug AS product_type
FROM {$wpdb->prefix}term_relationships AS tr
INNER JOIN {$wpdb->prefix}term_taxonomy AS x
ON ( x.taxonomy = 'product_type' AND x.term_taxonomy_id = tr.term_taxonomy_id )
INNER JOIN {$wpdb->prefix}terms AS t
ON t.term_id = x.term_id
) AS mo
ON product.id = mo.product_id
LEFT JOIN (
SELECT wcoitems.order_id as subscription_id, wcoimeta.meta_value as product_id, wcoimeta.order_item_id, wcoimeta2.meta_value as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE wcoitems.order_item_type = 'line_item'
AND ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
AND wcoimeta2.meta_key = '_line_total'
) as subscription_line_items
ON product.id = subscription_line_items.product_id
LEFT JOIN {$wpdb->posts} as subscriptions
ON subscriptions.ID = subscription_line_items.subscription_id
WHERE product.post_status = 'publish'
AND ( product.post_type = 'product' OR product.post_type = 'product_variation' )
AND subscriptions.post_type = 'shop_subscription'
AND subscriptions.post_status NOT IN( 'wc-pending', 'trash' )
GROUP BY product.id
ORDER BY COUNT(subscription_line_items.subscription_id) DESC" );
$cached_results = get_transient( strtolower( __CLASS__ ) );
$query_hash = md5( $query );
// Set a default value for cached results for PHP 8.2+ compatibility.
if ( empty( $cached_results ) ) {
$cached_results = [];
}
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_data', $wpdb->get_results( $query, OBJECT_K ), $args );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
}
$report_data = $cached_results[ $query_hash ];
// Organize subscription variations under the parent product in a tree structure
$tree = array();
foreach ( $report_data as $product_id => $product ) {
if ( ! $product->parent_product_id ) {
if ( isset( $tree[ $product_id ] ) ) {
array_unshift( $tree[ $product_id ], $product_id );
} else {
$tree[ $product_id ][] = $product_id;
}
} else {
$tree[ $product->parent_product_id ][] = $product_id;
}
}
// Create an array with all the report data in the correct order
$ordered_report_data = array();
foreach ( $tree as $parent_id => $children ) {
foreach ( $children as $child_id ) {
$ordered_report_data[ $child_id ] = $report_data[ $child_id ];
// When there are variations, store the variation ids.
if ( 'variable-subscription' === $report_data[ $child_id ]->product_type ) {
$ordered_report_data[ $child_id ]->variations = array_diff( $children, array( $parent_id ) );
}
}
}
$placeholders = implode( ',', array_fill( 0, count( $args['order_status'] ), '%s' ) );
$statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
// Now let's get the total revenue for each product so we can provide an average lifetime value for that product
$query = apply_filters( 'wcs_reports_product_lifetime_value_query',
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Ignored for allowing interpolation in the IN statements.
$wpdb->prepare(
"SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->posts} AS wcorders
ON wcoitems.order_id = wcorders.ID
AND wcorders.post_type = 'shop_order'
AND wcorders.post_status IN ( {$placeholders} )
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
AND wcoimeta2.meta_key = '_line_total'
GROUP BY product_id",
$statuses
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$query_hash = md5( $query );
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_product_lifetime_value_data', $wpdb->get_results( $query, OBJECT_K ), $args );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
}
self::init_cache();
$subscriptions_by_product = self::fetch_subscription_products_data( $args );
$subscription_product_totals = self::fetch_product_totals_data( $args );
$ordered_report_data = self::organize_subscription_products_data( $subscriptions_by_product );
// Add the product total to each item
foreach ( array_keys( $ordered_report_data ) as $product_id ) {
$ordered_report_data[ $product_id ]->product_total = isset( $cached_results[ $query_hash ][ $product_id ] ) ? $cached_results[ $query_hash ][ $product_id ]->product_total : 0;
$ordered_report_data[ $product_id ]->product_total = isset( $subscription_product_totals[ $product_id ] ) ? $subscription_product_totals[ $product_id ]->product_total : 0;
}
return $ordered_report_data;
@@ -280,11 +189,11 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
jQuery('.chart-placeholder.variation_breakdown_chart'),
[
<?php
$colorindex = -1;
$colorindex = -1;
$last_parent_id = -1;
foreach ( $variations as $product ) {
if ( '0' === $product->parent_product_id || $last_parent_id !== $product->parent_product_id ) {
$colorindex++;
++$colorindex;
$last_parent_id = $product->parent_product_id;
}
?>
@@ -334,7 +243,7 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
color: '<?php echo esc_js( $chart_colors[ $i ] ); ?>'
},
<?php
$i++;
++$i;
}
?>
],
@@ -371,9 +280,261 @@ class WCS_Report_Subscription_By_Product extends WP_List_Table {
/**
* Clears the cached report data.
*
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager before updating the cache.
*
* @since 3.0.10
*/
public static function clear_cache() {
delete_transient( strtolower( __CLASS__ ) );
self::$cached_report_results = array();
}
private static function fetch_subscription_products_data( $args = array() ) {
global $wpdb;
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = "SELECT product.id as product_id,
product.post_parent as parent_product_id,
product.post_title as product_name,
mo.product_type,
COUNT(subscription_line_items.subscription_id) as subscription_count,
SUM(subscription_line_items.product_total) as recurring_total
FROM {$wpdb->posts} AS product
LEFT JOIN (
SELECT tr.object_id AS product_id, t.slug AS product_type
FROM {$wpdb->prefix}term_relationships AS tr
INNER JOIN {$wpdb->prefix}term_taxonomy AS x
ON ( x.taxonomy = 'product_type' AND x.term_taxonomy_id = tr.term_taxonomy_id )
INNER JOIN {$wpdb->prefix}terms AS t
ON t.term_id = x.term_id
) AS mo
ON product.id = mo.product_id
LEFT JOIN (
SELECT wcoitems.order_id as subscription_id, wcoimeta.meta_value as product_id, wcoimeta.order_item_id, wcoimeta2.meta_value as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE wcoitems.order_item_type = 'line_item'
AND ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
AND wcoimeta2.meta_key = '_line_total'
) as subscription_line_items
ON product.id = subscription_line_items.product_id
LEFT JOIN {$wpdb->prefix}wc_orders as subscriptions
ON subscriptions.ID = subscription_line_items.subscription_id
WHERE product.post_status = 'publish'
AND ( product.post_type = 'product' OR product.post_type = 'product_variation' )
AND subscriptions.type = 'shop_subscription'
AND subscriptions.status NOT IN( 'wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash' )
GROUP BY product.id
ORDER BY COUNT(subscription_line_items.subscription_id) DESC";
} else {
$query = "SELECT product.id as product_id,
product.post_parent as parent_product_id,
product.post_title as product_name,
mo.product_type,
COUNT(subscription_line_items.subscription_id) as subscription_count,
SUM(subscription_line_items.product_total) as recurring_total
FROM {$wpdb->posts} AS product
LEFT JOIN (
SELECT tr.object_id AS product_id, t.slug AS product_type
FROM {$wpdb->prefix}term_relationships AS tr
INNER JOIN {$wpdb->prefix}term_taxonomy AS x
ON ( x.taxonomy = 'product_type' AND x.term_taxonomy_id = tr.term_taxonomy_id )
INNER JOIN {$wpdb->prefix}terms AS t
ON t.term_id = x.term_id
) AS mo
ON product.id = mo.product_id
LEFT JOIN (
SELECT wcoitems.order_id as subscription_id, wcoimeta.meta_value as product_id, wcoimeta.order_item_id, wcoimeta2.meta_value as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE wcoitems.order_item_type = 'line_item'
AND ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
AND wcoimeta2.meta_key = '_line_total'
) as subscription_line_items
ON product.id = subscription_line_items.product_id
LEFT JOIN {$wpdb->posts} as subscriptions
ON subscriptions.ID = subscription_line_items.subscription_id
WHERE product.post_status = 'publish'
AND ( product.post_type = 'product' OR product.post_type = 'product_variation' )
AND subscriptions.post_type = 'shop_subscription'
AND subscriptions.post_status NOT IN( 'wc-pending', 'auto-draft', 'wc-checkout-draft', 'trash' )
GROUP BY product.id
ORDER BY COUNT(subscription_line_items.subscription_id) DESC";
}
/**
* Filter the query to get the subscription products data.
*
* @param string $query The query to get the subscription products data.
* @return string The filtered query.
*
* @since 2.1.0
*/
$query = apply_filters( 'wcs_reports_product_query', $query );
$query_hash = md5( $query );
// We expect that cache was initialized before calling this method.
// Skip running the query if cache is available.
if ( $args['no_cache'] || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$query_results = (array) $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
/**
* Filter the query results for subscription products.
*
* @param array $query_results The query results.
* @param array $args The arguments for the report.
* @return array The filtered query results.
*
* @since 2.1.0
*/
$query_results = apply_filters( 'wcs_reports_product_data', $query_results, $args );
self::cache_report_results( $query_hash, $query_results );
}
return self::$cached_report_results[ $query_hash ];
}
/**
* Organize subscription products data for futher reporting.
*
* Group subscription product variations under variable subscription products.
*
* @param array $report_data The report data.
* @return array The organized report data.
*/
private static function organize_subscription_products_data( $report_data ) {
$tree = array();
foreach ( $report_data as $product_id => $product ) {
if ( ! $product->parent_product_id ) {
if ( isset( $tree[ $product_id ] ) ) {
array_unshift( $tree[ $product_id ], $product_id );
} else {
$tree[ $product_id ][] = $product_id;
}
} else {
$tree[ $product->parent_product_id ][] = $product_id;
}
}
// Create an array with all the report data in the correct order
$ordered_report_data = array();
foreach ( $tree as $parent_id => $children ) {
foreach ( $children as $child_id ) {
$ordered_report_data[ $child_id ] = $report_data[ $child_id ];
// When there are variations, store the variation ids.
if ( 'variable-subscription' === $report_data[ $child_id ]->product_type ) {
$ordered_report_data[ $child_id ]->variations = array_diff( $children, array( $parent_id ) );
}
}
}
return $ordered_report_data;
}
private static function fetch_product_totals_data( $args = array() ) {
global $wpdb;
$placeholders = implode( ',', array_fill( 0, count( $args['order_status'] ), '%s' ) );
$statuses = wcs_maybe_prefix_key( $args['order_status'], 'wc-' );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Ignored for allowing interpolation in the IN statements.
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->prefix}wc_orders AS wcorders
ON wcoitems.order_id = wcorders.ID
AND wcorders.type = 'shop_order'
AND wcorders.status IN ( {$placeholders} )
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
AND wcoimeta2.meta_key = '_line_total'
GROUP BY product_id",
$statuses
);
} else {
$query = $wpdb->prepare(
"SELECT wcoimeta.meta_value as product_id, SUM(wcoimeta2.meta_value) as product_total
FROM {$wpdb->prefix}woocommerce_order_items AS wcoitems
INNER JOIN {$wpdb->posts} AS wcorders
ON wcoitems.order_id = wcorders.ID
AND wcorders.post_type = 'shop_order'
AND wcorders.post_status IN ( {$placeholders} )
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta
ON wcoimeta.order_item_id = wcoitems.order_item_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS wcoimeta2
ON wcoimeta2.order_item_id = wcoitems.order_item_id
WHERE ( wcoimeta.meta_key = '_product_id' OR wcoimeta.meta_key = '_variation_id' )
AND wcoimeta2.meta_key = '_line_total'
GROUP BY product_id",
$statuses
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
/**
* Filter the query to get the product totals data.
*
* @param string $query The query to get the product totals data.
* @return string The filtered query.
*
* @since 2.1.0
*/
$query = apply_filters( 'wcs_reports_product_lifetime_value_query', $query );
$query_hash = md5( $query );
// We expect that cache was initialized before calling this method.
// Skip running the query if cache is available.
if ( $args['no_cache'] || ! isset( self::$cached_report_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$query_results = (array) $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
/**
* Filter the query results for product totals.
*
* @param array $query_results The query results.
* @param array $args The arguments for the report.
* @return array The filtered query results.
*
* @since 2.1.0
*/
$query_results = apply_filters( 'wcs_reports_product_lifetime_value_data', $query_results, $args );
self::cache_report_results( $query_hash, $query_results );
}
return self::$cached_report_results[ $query_hash ];
}
/**
* Initialize cache for report results.
*/
private static function init_cache() {
self::$cached_report_results = get_transient( strtolower( __CLASS__ ) );
// Set a default value for cached results for PHP 8.2+ compatibility.
if ( empty( self::$cached_report_results ) ) {
self::$cached_report_results = array();
}
}
/**
* Cache report results.
*
* @param string $query_hash The query hash.
* @param array $report_data The report data.
*/
private static function cache_report_results( $query_hash, $report_data ) {
self::$cached_report_results[ $query_hash ] = $report_data;
set_transient( strtolower( __CLASS__ ), self::$cached_report_results, WEEK_IN_SECONDS );
}
}

View File

@@ -36,71 +36,35 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
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 );
$offset = get_option( 'gmt_offset' );
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
$retry_date_in_local_time_query = $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})";
$this->group_by_query = "YEAR({$retry_date_in_local_time_query}), MONTH({$retry_date_in_local_time_query}), DAY({$retry_date_in_local_time_query})";
break;
case 'month':
$this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time})";
$this->group_by_query = "YEAR({$retry_date_in_local_time_query}), MONTH({$retry_date_in_local_time_query})";
break;
}
$this->report_data = new stdClass;
$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 retries (i.e. retries which eventually succeeded in processing the failed payment)
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
$this->report_data->renewal_data = $wpdb->get_results(
$wpdb->prepare(
"
SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN(%s) 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
",
$retry_date_in_local_time,
$query_start_date,
$query_end_date
)
$query_options = array(
'site_timezone' => $site_timezone,
'query_start_date' => get_gmt_from_date( date( 'Y-m-d H:i:s', $this->start_date ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
'query_end_date' => get_gmt_from_date( date( 'Y-m-d H:i:s', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// Get the counts for all retries, grouped by day or month and status
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
$this->report_data->retry_data = $wpdb->get_results(
$wpdb->prepare(
"
SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN(%s) 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
",
$retry_date_in_local_time,
$query_start_date,
$query_end_date
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->fetch_renewal_data( $query_options );
$this->fetch_retry_data( $query_options );
// Total up the query data
$this->report_data->retry_failed_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'count' ) ) );
$this->report_data->retry_success_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'count' ) ) );
$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->retry_failed_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'count' ) ) );
$this->report_data->retry_success_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'count' ) ) );
$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 = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) );
@@ -414,4 +378,86 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
return wc_format_decimal( $amount, wc_get_price_decimals() );
}
}
/**
* Get the sum of order totals for completed retries (i.e. retries which eventually succeeded in processing the failed payment)
*
* @param array $query_options Query options.
*/
private function fetch_renewal_data( $query_options ) {
global $wpdb;
$site_timezone = $query_options['site_timezone'];
$query_start_date = $query_options['query_start_date'];
$query_end_date = $query_options['query_end_date'];
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"
SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN(CONVERT_TZ(retries.date_gmt, '+00:00', %s)) AS retry_date, SUM(orders.total_amount) AS renewal_totals
FROM {$wpdb->prefix}wcs_payment_retries AS retries
INNER JOIN {$wpdb->prefix}wc_orders AS orders ON ( orders.id = retries.order_id )
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
",
$site_timezone,
$query_start_date,
$query_end_date
);
} else {
$query = $wpdb->prepare(
"
SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN(CONVERT_TZ(retries.date_gmt, '+00:00', %s)) 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
",
$site_timezone,
$query_start_date,
$query_end_date
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->report_data->renewal_data = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get the counts for all retries, grouped by day or month and status
*
* @param array $query_options Query options.
*/
private function fetch_retry_data( $query_options ) {
global $wpdb;
$site_timezone = $query_options['site_timezone'];
$query_start_date = $query_options['query_start_date'];
$query_end_date = $query_options['query_end_date'];
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
// We don't use HPOS tables here, so we can use it for both CPT and HPOS data stores.
$query = $wpdb->prepare(
"
SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN(CONVERT_TZ(retries.date_gmt, '+00:00', %s)) 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
",
$site_timezone,
$query_start_date,
$query_end_date
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->report_data->retry_data = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
}

View File

@@ -117,7 +117,11 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
/**
* Get report data.
* @return stdClass
*
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager to update the cache.
*
* @param array $args The arguments for the report.
* @return stdClass[] - Upcoming renewal data grouped by scheduled date.
*/
public function get_data( $args = array() ) {
global $wpdb;
@@ -131,55 +135,99 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
// Query based on whole days, not minutes/hours so that we can cache the query for at least 24 hours
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The $this->group_by_query clause is hard coded.
$base_query = $wpdb->prepare(
"SELECT
DATE(ms.meta_value) as scheduled_date,
SUM(mo.meta_value) as recurring_total,
COUNT(mo.meta_value) as total_renewals,
group_concat(p.ID) as subscription_ids,
group_concat(mi.meta_value) as billing_intervals,
group_concat(mp.meta_value) as billing_periods,
group_concat(me.meta_value) as scheduled_ends,
group_concat(mo.meta_value) as subscription_totals
FROM {$wpdb->prefix}posts p
LEFT JOIN {$wpdb->prefix}postmeta ms
ON p.ID = ms.post_id
LEFT JOIN {$wpdb->prefix}postmeta mo
ON p.ID = mo.post_id
LEFT JOIN {$wpdb->prefix}postmeta mi
ON p.ID = mi.post_id
LEFT JOIN {$wpdb->prefix}postmeta mp
ON p.ID = mp.post_id
LEFT JOIN {$wpdb->prefix}postmeta me
ON p.ID = me.post_id
WHERE p.post_type = 'shop_subscription'
AND p.post_status = 'wc-active'
AND mo.meta_key = '_order_total'
AND ms.meta_key = '_schedule_next_payment'
AND ( ( ms.meta_value < %s AND me.meta_value = 0 ) OR ( me.meta_value > %s AND ms.meta_value < %s ) )
AND mi.meta_key = '_billing_interval'
AND mp.meta_key = '_billing_period'
AND me.meta_key = '_schedule_end '
GROUP BY {$this->group_by_query}
ORDER BY ms.meta_value ASC",
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ),
date( 'Y-m-d', $this->start_date ),
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) )
);
if ( wcs_is_custom_order_tables_usage_enabled() ) {
$query = $wpdb->prepare(
"SELECT
DATE(meta_next_payment.meta_value) as scheduled_date,
SUM(subscriptions.total_amount) as recurring_total,
COUNT(subscriptions.total_amount) as total_renewals,
group_concat(subscriptions.ID) as subscription_ids,
group_concat(meta_billing_interval.meta_value) as billing_intervals,
group_concat(meta_billing_period.meta_value) as billing_periods,
group_concat(meta_schedule_end.meta_value) as scheduled_ends,
group_concat(subscriptions.total_amount) as subscription_totals
FROM {$wpdb->prefix}wc_orders subscriptions
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta_next_payment
ON subscriptions.ID = meta_next_payment.order_id
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta_billing_interval
ON subscriptions.ID = meta_billing_interval.order_id
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta_billing_period
ON subscriptions.ID = meta_billing_period.order_id
LEFT JOIN {$wpdb->prefix}wc_orders_meta meta_schedule_end
ON subscriptions.ID = meta_schedule_end.order_id
WHERE subscriptions.type = 'shop_subscription'
AND subscriptions.status = 'wc-active'
AND meta_next_payment.meta_key = '_schedule_next_payment'
AND ( ( meta_next_payment.meta_value < %s AND meta_schedule_end.meta_value = 0 ) OR ( meta_schedule_end.meta_value > %s AND meta_next_payment.meta_value < %s ) )
AND meta_billing_interval.meta_key = '_billing_interval'
AND meta_billing_period.meta_key = '_billing_period'
AND meta_schedule_end.meta_key = '_schedule_end'
GROUP BY {$this->group_by_query}
ORDER BY meta_next_payment.meta_value ASC",
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
date( 'Y-m-d', $this->start_date ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
);
} else {
$query = $wpdb->prepare(
"SELECT
DATE(meta_next_payment.meta_value) as scheduled_date,
SUM(meta_order_total.meta_value) as recurring_total,
COUNT(meta_order_total.meta_value) as total_renewals,
group_concat(posts.ID) as subscription_ids,
group_concat(meta_billing_interval.meta_value) as billing_intervals,
group_concat(meta_billing_period.meta_value) as billing_periods,
group_concat(meta_schedule_end.meta_value) as scheduled_ends,
group_concat(meta_order_total.meta_value) as subscription_totals
FROM {$wpdb->prefix}posts posts
LEFT JOIN {$wpdb->prefix}postmeta meta_next_payment
ON posts.ID = meta_next_payment.post_id
LEFT JOIN {$wpdb->prefix}postmeta meta_order_total
ON posts.ID = meta_order_total.post_id
LEFT JOIN {$wpdb->prefix}postmeta meta_billing_interval
ON posts.ID = meta_billing_interval.post_id
LEFT JOIN {$wpdb->prefix}postmeta meta_billing_period
ON posts.ID = meta_billing_period.post_id
LEFT JOIN {$wpdb->prefix}postmeta meta_schedule_end
ON posts.ID = meta_schedule_end.post_id
WHERE posts.post_type = 'shop_subscription'
AND posts.post_status = 'wc-active'
AND meta_order_total.meta_key = '_order_total'
AND meta_next_payment.meta_key = '_schedule_next_payment'
AND ( ( meta_next_payment.meta_value < %s AND meta_schedule_end.meta_value = 0 ) OR ( meta_schedule_end.meta_value > %s AND meta_next_payment.meta_value < %s ) )
AND meta_billing_interval.meta_key = '_billing_interval'
AND meta_billing_period.meta_key = '_billing_period'
AND meta_schedule_end.meta_key = '_schedule_end'
GROUP BY {$this->group_by_query}
ORDER BY meta_next_payment.meta_value ASC",
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
date( 'Y-m-d', $this->start_date ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Keep date formatting from original report.
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$cached_results = get_transient( strtolower( get_class( $this ) ) );
$query_hash = md5( $base_query );
$query_hash = md5( $query );
// Set a default value for cached results for PHP 8.2+ compatibility.
if ( empty( $cached_results ) ) {
$cached_results = [];
$cached_results = array();
}
if ( $args['no_cache'] || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
$cached_results[ $query_hash ] = apply_filters( 'wcs_reports_upcoming_recurring_revenue_data', $wpdb->get_results( $base_query, OBJECT_K ), $args );
$results = $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This query is prepared above.
/**
* Filter the upcoming recurring revenue data.
*
* @param array $results The upcoming recurring revenue data.
* @param array $args The arguments for the report.
* @since 2.1.0
*/
$results = apply_filters( 'wcs_reports_upcoming_recurring_revenue_data', $results, $args );
$cached_results[ $query_hash ] = $results;
set_transient( strtolower( get_class( $this ) ), $cached_results, WEEK_IN_SECONDS );
}
@@ -418,12 +466,12 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
// Group by
switch ( $this->chart_groupby ) {
case 'day':
$this->group_by_query = 'YEAR(ms.meta_value), MONTH(ms.meta_value), DAY(ms.meta_value)';
$this->group_by_query = 'YEAR(meta_next_payment.meta_value), MONTH(meta_next_payment.meta_value), DAY(meta_next_payment.meta_value)';
$this->chart_interval = ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) );
$this->barwidth = 60 * 60 * 24 * 1000;
break;
case 'month':
$this->group_by_query = 'YEAR(ms.meta_value), MONTH(ms.meta_value), DAY(ms.meta_value)';
$this->group_by_query = 'YEAR(meta_next_payment.meta_value), MONTH(meta_next_payment.meta_value), DAY(meta_next_payment.meta_value)';
$this->chart_interval = 0;
$min_date = $this->start_date;
while ( ( $min_date = wcs_add_months( $min_date, '1' ) ) <= $this->end_date ) {
@@ -450,6 +498,8 @@ class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report {
/**
* Clears the cached query results.
*
* @see WCS_Report_Cache_Manager::update_cache() - This method is called by the cache manager before updating the cache.
*
* @since 3.0.10
*/
public function clear_cache() {

View File

@@ -0,0 +1,115 @@
<?php
/**
* Class WC_REST_Subscriptions_Settings_Option_Controller
*/
defined( 'ABSPATH' ) || exit;
/**
* REST controller for settings options.
*/
class WC_REST_Subscriptions_Settings_Option_Controller extends WP_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* List of allowed option names that can be updated via the REST API.
*
* @var array
*/
private const ALLOWED_OPTIONS = [
'woocommerce_subscriptions_gifting_is_welcome_announcement_dismissed',
];
/**
* Endpoint path.
*
* @var string
*/
protected $rest_base = 'subscriptions/settings';
/**
* Configure REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<option_name>[a-zA-Z0-9_-]+)',
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'update_option' ],
'permission_callback' => [ $this, 'check_permission' ],
'args' => [
'option_name' => [
'required' => true,
'validate_callback' => [ $this, 'validate_option_name' ],
],
'value' => [
'required' => true,
'validate_callback' => [ $this, 'validate_value' ],
],
],
]
);
}
/**
* Validate the option name.
*
* @param string $option_name The option name to validate.
* @return bool
*/
public function validate_option_name( string $option_name ): bool {
return in_array( $option_name, self::ALLOWED_OPTIONS, true );
}
/**
* Validate the value parameter.
*
* @param mixed $value The value to validate.
* @return bool|WP_Error True if valid, WP_Error if invalid.
*/
public function validate_value( $value ) {
if ( is_bool( $value ) || is_array( $value ) ) {
return true;
}
return new WP_Error(
'rest_invalid_param',
__( 'Invalid value type; must be either boolean or array', 'woocommerce-subscriptions' ),
[ 'status' => 400 ]
);
}
/**
* Update the option value.
*
* @param WP_REST_Request $request The request object.
* @return WP_Error|WP_REST_Response
*/
public function update_option( WP_REST_Request $request ) {
$option_name = $request->get_param( 'option_name' );
$value = $request->get_param( 'value' );
update_option( $option_name, $value );
return rest_ensure_response(
[
'success' => true,
]
);
}
/**
* Verify access.
*
* Override this method if custom permissions required.
*/
public function check_permission() {
return current_user_can( 'manage_woocommerce' );
}
}

View File

@@ -27,11 +27,28 @@ class WC_Subscriptions_Dependency_Manager {
*/
private $wc_version_cached = false;
/**
* @var boolean Whether to skip the class_exists and WC_VERSION constant checks.
*/
private $skip_class_exists_and_wc_version_constant_checks = false;
/**
* Constructor.
*/
public function __construct( $minimum_supported_wc_version ) {
$this->minimum_supported_wc_version = $minimum_supported_wc_version;
/**
* Filter allows to skip the class_exists and WC_VERSION constant checks.
*
* @since 7.8.0
*
* @param bool $use_class_exists Whether to use the class_exists and WC_VERSION constant checks.
*
* @return bool false to use the class_exists and WC_VERSION checks, true to skip them.
*/
if ( defined( 'WCS_ENVIRONMENT_TYPE' ) && WCS_ENVIRONMENT_TYPE === 'tests' && apply_filters( 'woocommerce_subscriptions_skip_class_exists_and_wc_version_constant_checks', false ) ) {
$this->skip_class_exists_and_wc_version_constant_checks = true;
}
}
/**
@@ -52,7 +69,7 @@ class WC_Subscriptions_Dependency_Manager {
* @return bool True if the plugin is active, false otherwise.
*/
public function is_woocommerce_active() {
if ( class_exists( 'WooCommerce' ) ) {
if ( class_exists( 'WooCommerce' ) && ! $this->skip_class_exists_and_wc_version_constant_checks ) {
return true;
}
@@ -89,7 +106,7 @@ class WC_Subscriptions_Dependency_Manager {
* @return string|null The active WooCommerce version, or null if WooCommerce is not active.
*/
private function get_woocommerce_active_version() {
if ( defined( 'WC_VERSION' ) ) {
if ( defined( 'WC_VERSION' ) && ! $this->skip_class_exists_and_wc_version_constant_checks ) {
return WC_VERSION;
}

View File

@@ -38,6 +38,7 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
}
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_show_welcome_message' ) );
add_action( 'plugins_loaded', array( $this, 'init_gifting' ), 20 );
}
/**
@@ -232,4 +233,109 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
</div>
<?php
}
/**
* Attempts to initialize gifting functionality.
*
* Before doing this, the method tries to determine if the standalone WooCommerce Gifting plugin is active and has
* already loaded (if the standalone plugin is active, we do not proceed). To accomplish this, this method expects
* to run during plugins_loaded at priority 20 (whereas the equivalent code from the standalone plugin will run at
* priority 11).
*/
public function init_gifting() {
if ( ! WCSG_Admin_Welcome_Announcement::is_welcome_announcement_dismissed() ) {
WCSG_Admin_Welcome_Announcement::init();
}
if (
$this->is_plugin_being_activated( 'woocommerce-subscriptions-gifting' )
|| function_exists( 'wcsg_load' )
) {
return;
}
$gifting_includes = trailingslashit( $this->get_plugin_directory( 'includes/gifting' ) );
require_once $gifting_includes . 'wcsg-compatibility-functions.php';
WCSG_Product::init();
WCSG_Cart::init();
WCSG_Checkout::init();
WCSG_Recipient_Management::init();
WCSG_Recipient_Details::init();
WCSG_Email::init();
WCSG_Download_Handler::init();
WCSG_Admin::init();
WCSG_Recipient_Addresses::init();
WCSG_Template_Loader::init();
WCSG_Admin_System_Status::init();
add_action(
'init',
function () {
new WCSG_Privacy();
}
);
WCS_Gifting::init();
}
/**
* Tries to determine if the specified plugin is being activated.
*
* The provided plugin slug can be either the complete relative plugin path (ie, 'plugin-slug/plugin-slug.php') or
* just a part of the path (ie, 'plugin-slug'). So long as the plugin which is actually being activated contains
* that string, then we consider ourselves to have a match and will return true.
*
* Therefore, consider with care how precise you need to be: something highly specific like our first example will
* fail if the plugin directory has been renamed. A shorter fragment, on the other hand, will potentially match the
* wrong plugin.
*
* This method is only useful as a means of detecting when a plugin is activated through 'conventional' means (via
* the plugin admin screen, or via WP CLI), but it will not provide protection if, for example, third party code
* makes its own arbitrary calls to activate_plugin().
*
* @param string $plugin_slug Plugin slug.
*
* @return bool
*/
private function is_plugin_being_activated( string $plugin_slug ): bool {
// Try to determine if a plugin is in the process of being activated via the plugin admin screen.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$action = isset( $_REQUEST['action'] ) ? wc_clean( wp_unslash( $_REQUEST['action'] ) ) : '';
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$plugin = isset( $_REQUEST['plugin'] ) ? wc_clean( wp_unslash( $_REQUEST['plugin'] ) ) : '';
if (
'activate' === $action
&& str_contains( $plugin, $plugin_slug )
) {
return true;
}
// Try to determine if a plugin is being activated via WP CLI.
if ( class_exists( WP_CLI::class ) ) {
// Note that flags such as `--no-color` are filtered out of this array.
$args = WP_CLI::get_runner()->arguments;
if (
count( $args ) < 3
|| $args[0] !== 'plugin'
|| $args[1] !== 'activate'
) {
return false;
}
// The remaining arguments are the list of plugin slugs to be activated.
$plugins_to_be_activated = array_slice( $args, 2 );
foreach ( $plugins_to_be_activated as $plugin ) {
if ( str_contains( $plugin, $plugin_slug ) ) {
return true;
}
}
}
return false;
}
}

View File

@@ -60,6 +60,7 @@ class WCS_API {
// V3 (latest)
'WC_REST_Subscriptions_Controller',
'WC_REST_Subscription_notes_Controller',
'WC_REST_Subscriptions_Settings_Option_Controller',
);
foreach ( $endpoint_classes as $class ) {

View File

@@ -41,10 +41,10 @@ class WCS_Call_To_Action_Button_Text_Manager {
'tip' => '',
'id' => WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text',
'css' => 'min-width:150px;',
'default' => __( 'Sign up now', 'woocommerce-subscriptions' ),
'default' => __( 'Add to cart', 'woocommerce-subscriptions' ),
'type' => 'text',
'desc_tip' => true,
'placeholder' => __( 'Sign up now', 'woocommerce-subscriptions' ),
'placeholder' => __( 'Add to cart', 'woocommerce-subscriptions' ),
),
array(
'name' => __( 'Place Order Button Text', 'woocommerce-subscriptions' ),
@@ -52,10 +52,10 @@ class WCS_Call_To_Action_Button_Text_Manager {
'tip' => '',
'id' => WC_Subscriptions_Admin::$option_prefix . '_order_button_text',
'css' => 'min-width:150px;',
'default' => __( 'Sign up now', 'woocommerce-subscriptions' ),
'default' => __( 'Add to cart', 'woocommerce-subscriptions' ),
'type' => 'text',
'desc_tip' => true,
'placeholder' => __( 'Sign up now', 'woocommerce-subscriptions' ),
'placeholder' => __( 'Add to cart', 'woocommerce-subscriptions' ),
),
array(
'type' => 'sectionend',

View File

@@ -20,13 +20,6 @@ abstract class WCS_Cache_Manager {
return new $manager();
}
/**
* WCS_Cache_Manager constructor.
*
* Loads the logger if it's not overwritten.
*/
abstract function __construct();
/**
* Initialises some form of logger
*/

View File

@@ -389,6 +389,34 @@ class WC_Subscriptions_Admin {
</p>
<?php
// Maybe show gifting options. The method_exists check is required for cases where the standalone Gifting
// extension is active (in which case, a different version of WCSG_Admin will be loaded).
if ( method_exists( WCSG_Admin::class, 'is_gifting_enabled' ) && WCSG_Admin::is_gifting_enabled() ) {
$product_gifting = WC_Subscriptions_Product::get_gifting( $post->ID );
$is_following_gifting_global_setting = empty( $product_gifting );
woocommerce_wp_select(
array(
'id' => '_subscription_gifting',
'class' => 'select short wc-enhanced-select',
'wrapper_class' => '_subscription_gifting_field' . ( ! $is_following_gifting_global_setting ? ' overriding-store-settings' : '' ),
'label' => __( 'Gifting', 'woocommerce-subscriptions' ),
'value' => $product_gifting,
'options' => array(
'' => WCSG_Admin::get_gifting_option_text(),
'enabled' => __( 'Enabled', 'woocommerce-subscriptions' ),
'disabled' => __( 'Disabled', 'woocommerce-subscriptions' ),
),
'desc_tip' => true,
'description' => __( 'Allow shoppers to purchase a subscription as a gift.', 'woocommerce-subscriptions' ),
)
);
if ( ! $is_following_gifting_global_setting ) {
WCSG_Admin::get_gifting_global_override_text();
}
}
do_action( 'woocommerce_subscriptions_product_options_pricing' );
wp_nonce_field( 'wcs_subscription_meta', '_wcsnonce' );
@@ -429,7 +457,6 @@ class WC_Subscriptions_Admin {
if ( ! $needs_html_fix ) {
echo '</div>';
}
}
/**
@@ -560,6 +587,7 @@ class WC_Subscriptions_Admin {
'_subscription_trial_period',
'_subscription_limit',
'_subscription_one_time_shipping',
'_subscription_gifting',
);
foreach ( $subscription_fields as $field_name ) {
@@ -772,6 +800,7 @@ class WC_Subscriptions_Admin {
'_subscription_length',
'_subscription_trial_period',
'_subscription_trial_length',
'_subscription_gifting',
);
foreach ( $subscription_fields as $field_name ) {
@@ -938,8 +967,8 @@ class WC_Subscriptions_Admin {
}
if ( $is_woocommerce_screen || 'edit-product' == $screen->id || ( isset( $_GET['page'], $_GET['tab'] ) && 'wc-reports' === $_GET['page'] && 'subscriptions' === $_GET['tab'] ) ) {
wp_enqueue_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', [ 'wc-components' ], WC_Subscriptions_Core_Plugin::instance()->get_library_version() );
wp_enqueue_style( 'woocommerce_subscriptions_admin', WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( 'assets/css/admin.css' ), array( 'woocommerce_admin_styles' ), WC_Subscriptions_Core_Plugin::instance()->get_library_version() );
wp_enqueue_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', [ 'wc-components' ], WC_Subscriptions::$version );
wp_enqueue_style( 'woocommerce_subscriptions_admin', WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( 'assets/css/admin.css' ), array( 'woocommerce_admin_styles' ), WC_Subscriptions::$version );
}
}
@@ -1248,7 +1277,6 @@ class WC_Subscriptions_Admin {
),
)
);
}
/**

View File

@@ -16,7 +16,7 @@ class WC_Subscriptions_Cart_Validator {
*/
public static function init() {
add_filter( 'woocommerce_add_to_cart_validation', array( __CLASS__, 'maybe_empty_cart' ), 10, 5 );
add_filter( 'woocommerce_add_to_cart_validation', array( __CLASS__, 'maybe_empty_cart' ), 10, 6 );
add_filter( 'woocommerce_cart_loaded_from_session', array( __CLASS__, 'validate_cart_contents_for_mixed_checkout' ), 10 );
add_filter( 'woocommerce_add_to_cart_validation', array( __CLASS__, 'can_add_product_to_cart' ), 10, 6 );
@@ -28,9 +28,17 @@ class WC_Subscriptions_Cart_Validator {
*
* If multiple purchase flag is set, allow them to be added at the same time.
*
* @param bool $valid Whether the product can be added to the cart.
* @param int $product_id The product ID.
* @param int $quantity The quantity of the product being added.
* @param int $variation_id The variation ID.
* @param array $variations The variations of the product being added.
* @param array $item_data The additional item data set by all plugins.
*
* @return bool Whether the product can be added to the cart.
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.6.0
*/
public static function maybe_empty_cart( $valid, $product_id, $quantity, $variation_id = '', $variations = array() ) {
public static function maybe_empty_cart( $valid, $product_id, $quantity, $variation_id = 0, $variations = array(), $item_data = array() ) {
$is_subscription = WC_Subscriptions_Product::is_subscription( $product_id );
$cart_contains_subscription = WC_Subscriptions_Cart::cart_contains_subscription();
$payment_gateways_handler = WC_Subscriptions_Core_Plugin::instance()->get_gateways_handler_class();
@@ -38,6 +46,14 @@ class WC_Subscriptions_Cart_Validator {
$manual_renewals_enabled = wcs_is_manual_renewal_enabled();
$canonical_product_id = ! empty( $variation_id ) ? $variation_id : $product_id;
/**
* These flags are used by Product Bundles and Composite Products to indicate that the product is being added as part of an order again.
* We don't need to empty cart in this case but neither we need to add the product again.
*/
if ( isset( $item_data['is_order_again_composited'] ) || isset( $item_data['is_order_again_bundled'] ) ) {
return false;
}
if ( $is_subscription && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) {
// Generate a cart item key from variation and cart item data - which may be added by other plugins

View File

@@ -655,7 +655,14 @@ class WC_Subscriptions_Checkout {
return $button_text;
}
return apply_filters( 'wcs_place_subscription_order_text', __( 'Sign up now', 'woocommerce-subscriptions' ) );
/**
* Filter the "Add to cart" button text for subscription carts.
*
* @since 7.8.0
* @param string $button_text The "Add to cart" button text.
* @return string The "Add to cart" button text.
*/
return apply_filters( 'wcs_place_subscription_order_text', __( 'Add to cart', 'woocommerce-subscriptions' ) );
}
/**

View File

@@ -82,7 +82,6 @@ class WC_Subscriptions_Frontend_Scripts {
'media' => 'all',
);
}
return $styles;
}
}

View File

@@ -64,6 +64,8 @@ class WC_Subscriptions_Order {
add_action( 'woocommerce_subscription_details_after_subscription_table', __CLASS__ . '::get_related_orders_template' );
add_action( 'woocommerce_subscription_details_after_subscription_related_orders_table', __CLASS__ . '::get_related_orders_pagination_template', 5, 4 );
add_action( 'woocommerce_subscription_status_cancelled', array( __CLASS__, 'cancel_pending_related_orders' ), 10, 1 );
add_filter( 'woocommerce_my_account_my_orders_actions', __CLASS__ . '::maybe_remove_pay_action', 10, 2 );
add_action( 'woocommerce_order_fully_refunded', __CLASS__ . '::maybe_cancel_subscription_on_full_refund' );
@@ -382,7 +384,6 @@ class WC_Subscriptions_Order {
)
);
}
}
/**
@@ -589,6 +590,36 @@ class WC_Subscriptions_Order {
}
}
/**
* Cancel related orders when a subscription is cancelled.
*
* @param WC_Subscription $subscription The subscription that was cancelled.
*/
public static function cancel_pending_related_orders( $subscription ) {
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
wc_get_logger()->warning(
'Failed to cancel pending related orders on subscription cancellation. Subscription is not a WC_Subscription object.',
array(
'subscription' => $subscription,
),
);
return;
}
$related_orders = $subscription->get_related_orders( 'all', array( 'parent', 'renewal', 'switch', 'resubscribe' ) );
foreach ( $related_orders as $order ) {
if ( $order->has_status( 'pending' ) && $order->needs_payment() ) {
$note = sprintf(
// translators: %d: subscription ID.
__( 'Order cancelled due to subscription #%d cancellation.', 'woocommerce-subscriptions' ),
$subscription->get_id()
);
$order->update_status( 'cancelled', $note );
$order->save();
}
}
}
/* Order Price Getters */
/**

View File

@@ -23,6 +23,7 @@ class WC_Subscriptions_Product {
'_subscription_length',
'_subscription_trial_period',
'_subscription_trial_length',
'_subscription_gifting',
);
/**
@@ -114,6 +115,28 @@ class WC_Subscriptions_Product {
return apply_filters( 'woocommerce_is_subscription', $is_subscription, $product_id, $product );
}
/**
* Checks a given product to determine if it is a variable subscription.
*
* @param int|WC_Product $product Either a product object or product's post ID.
* @since 7.8.0
* @see WC_Subscriptions_Product::is_subscription()
*/
public static function is_variable_subscription( $product ) {
$is_variable_subscription = false;
$product = self::maybe_get_product_instance( $product );
if ( is_object( $product ) ) {
if ( $product->is_type( array( 'variable-subscription' ) ) ) {
$is_variable_subscription = true;
}
}
return $is_variable_subscription;
}
/**
* Output subscription string as the price html for grouped products and make sure that
* sign-up fees are taken into account for price.
@@ -531,6 +554,17 @@ class WC_Subscriptions_Product {
return apply_filters( 'woocommerce_subscriptions_product_sign_up_fee', self::get_meta_data( $product, 'subscription_sign_up_fee', 0, 'use_default_value' ), self::maybe_get_product_instance( $product ) );
}
/**
* Returns the gifting setting for a subscription, if it is a subscription.
*
* @param mixed $product A WC_Product object or product ID
* @return string The value of the gifting setting, or '' if the product it is using the global setting.
* @since 7.8.0
*/
public static function get_gifting( $product ) {
return apply_filters( 'woocommerce_subscriptions_product_gifting', self::get_meta_data( $product, 'subscription_gifting', '' ), self::maybe_get_product_instance( $product ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
/**
* Takes a subscription product's ID and returns the date on which the first renewal payment will be processed
* based on the subscription's length and calculated from either the $from_date if specified, or the current date/time.
@@ -1202,7 +1236,14 @@ class WC_Subscriptions_Product {
* @return string The add to cart text.
*/
public static function get_add_to_cart_text() {
return apply_filters( 'wc_subscription_product_add_to_cart_text', __( 'Sign up now', 'woocommerce-subscriptions' ) );
/**
* Filter the "Add to cart" button text for subscription products.
*
* @since 7.8.0
* @param string $button_text The "Add to cart" button text.
* @return string The "Add to cart" button text.
*/
return apply_filters( 'wc_subscription_product_add_to_cart_text', __( 'Add to cart', 'woocommerce-subscriptions' ) );
}
/**
@@ -1234,7 +1275,7 @@ class WC_Subscriptions_Product {
global $product;
if ( self::is_subscription( $product ) || in_array( $product_type, array( 'subscription', 'subscription-variation' ) ) ) {
$button_text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Sign up now', 'woocommerce-subscriptions' ) );
$button_text = get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text', __( 'Add to cart', 'woocommerce-subscriptions' ) );
}
return $button_text;

View File

@@ -822,9 +822,18 @@ class WC_Subscriptions_Synchroniser {
}
/**
* Return a string explaining when the first payment will be completed for the subscription.
* Return a string explaining when the first payment will be completed for a synchronized subscription product.
*
* For synchronized subscription products, this method calculates and formats a human-readable string
* indicating when the first payment will be processed. The string will indicate if the payment is:
* - Due today
* - Prorated with the next payment date
* - Just the first payment date
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v1.5
*
* @param WC_Product|WC_Product_Subscription|WC_Product_Variable_Subscription $product The subscription product to get the first payment date for.
* @return string A formatted string explaining the first payment date. Empty string if product is not synchronized.
*/
public static function get_products_first_payment_date( $product ) {

View File

@@ -83,6 +83,7 @@ class WCS_Blocks_Integration implements IntegrationInterface {
return array(
'woocommerce-subscriptions-blocks' => 'active',
'place_order_override' => $this->get_place_order_button_text_override(),
'gifting_checkbox_text' => apply_filters( 'wcsg_enable_gifting_checkbox_label', get_option( WCSG_Admin::$option_prefix . '_gifting_checkbox_text', __( 'This is a gift', 'woocommerce-subscriptions' ) ) ),
);
}

View File

@@ -163,26 +163,4 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde
$this->template_base
);
}
/**
* Gets the deprecated public variables for backwards compatibility.
*
* @param string $key Key.
*
* @return string|null
*/
public function __get( $key ) {
if ( 'heading_downloadable' === $key ) {
wcs_deprecated_argument( __CLASS__ . '::$' . $key, '5.6.0', 'The heading_downloadable property used for emails with downloadable files was removed in WooCommerce 3.1. Use the heading property instead.' );
return $this->get_option( 'heading_downloadable', _x( 'Your subscription renewal order is complete - download your files', 'Default email heading for email with downloadable files in it', 'woocommerce-subscriptions' ) );
} elseif ( 'subject_downloadable' === $key ) {
wcs_deprecated_argument( __CLASS__ . '::$' . $key, '5.6.0', 'The subject_downloadabl property used for emails with downloadable files was removed in WooCommerce 3.1. Use the subject property instead.' );
// translators: $1: {site_title}, $2: {order_date}, variables will be substituted when email is sent out
return $this->get_option( 'subject_downloadable', sprintf( _x( 'Your %1$s subscription renewal order from %2$s is complete - download your files', 'Default email subject for email with downloadable files in it', 'woocommerce-subscriptions' ), '{site_title}', '{order_date}' ) );
} else {
return;
}
}
}

View File

@@ -172,25 +172,4 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order
$this->template_base
);
}
/**
* Gets the deprecated public variables for backwards compatibility.
*
* @param string $key Key.
*
* @return string|null
*/
public function __get( $key ) {
if ( 'heading_downloadable' === $key ) {
wcs_deprecated_argument( __CLASS__ . '::$' . $key, '5.6.0', 'The heading_downloadable property used for emails with downloadable files was removed in WooCommerce 3.1. Use the heading property instead.' );
return $this->get_option( 'heading_downloadable', __( 'Your subscription change is complete - download your files', 'woocommerce-subscriptions' ) );
} elseif ( 'subject_downloadable' === $key ) {
wcs_deprecated_argument( __CLASS__ . '::$' . $key, '5.6.0', 'The subject_downloadable property used for emails with downloadable files was removed in WooCommerce 3.1. Use the subject property instead.' );
return $this->get_option( 'subject_downloadable', __( 'Your {site_title} subscription change from {order_date} is complete - download your files', 'woocommerce-subscriptions' ) );
} else {
return;
}
}
}

View File

@@ -46,7 +46,7 @@ class WCS_Array_Property_Post_Meta_Black_Magic implements ArrayAccess {
*/
#[\ReturnTypeWillChange]
public function offsetGet( $key ) {
return get_post_meta( $this->product_id, $this->maybe_prefix_meta_key( $key ) );
return get_post_meta( $this->product_id, $this->maybe_prefix_meta_key( $key ), true );
}
/**

View File

@@ -138,7 +138,10 @@ class WC_Subscriptions_Upgrader {
self::legacy_core_library_upgrades();
}
self::plugin_upgrades();
if ( self::$plugin_upgrades_may_be_needed ) {
self::plugin_upgrades();
}
self::upgrade_complete();
}
@@ -219,8 +222,10 @@ class WC_Subscriptions_Upgrader {
* @return void
*/
private static function plugin_upgrades(): void {
// This method is currently just a placeholder. Once we're ready to add a migration in a future release, this
// comment can of course be deleted.
// Enable Gifting by default if the Gifting plugin is enabled.
if ( version_compare( self::$stored_plugin_version, '7.8.0', '<' ) ) {
WCS_Plugin_Upgrade_7_8_0::check_gifting_plugin_is_enabled();
}
}
/**
@@ -493,7 +498,7 @@ class WC_Subscriptions_Upgrader {
case 'really_old_version':
$upgraded_versions = self::upgrade_really_old_versions();
$results = array(
$results = array(
// translators: placeholder is a list of version numbers (e.g. "1.3 & 1.4 & 1.5")
'message' => sprintf( __( 'Database updated to version %s', 'woocommerce-subscriptions' ), $upgraded_versions ),
);
@@ -501,7 +506,7 @@ class WC_Subscriptions_Upgrader {
case 'products':
$upgraded_product_count = WCS_Upgrade_1_5::upgrade_products();
$results = array(
$results = array(
// translators: placeholder is number of upgraded subscriptions
'message' => sprintf( _x( 'Marked %s subscription products as "sold individually".', 'used in the subscriptions upgrader', 'woocommerce-subscriptions' ), $upgraded_product_count ),
);
@@ -509,7 +514,7 @@ class WC_Subscriptions_Upgrader {
case 'hooks':
$upgraded_hook_count = WCS_Upgrade_1_5::upgrade_hooks( self::$upgrade_limit_hooks );
$results = array(
$results = array(
'upgraded_count' => $upgraded_hook_count,
// translators: 1$: number of action scheduler hooks upgraded, 2$: "{execution_time}", will be replaced on front end with actual time
'message' => sprintf( __( 'Migrated %1$s subscription related hooks to the new scheduler (in %2$s seconds).', 'woocommerce-subscriptions' ), $upgraded_hook_count, '{execution_time}' ),
@@ -763,7 +768,7 @@ class WC_Subscriptions_Upgrader {
$about_page_url = self::$about_page_url;
@header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
include_once( dirname( __FILE__ ) . '/templates/wcs-upgrade.php' );
include_once __DIR__ . '/templates/wcs-upgrade.php';
WCS_Upgrade_Logger::add( 'Loaded database upgrade helper' );
}
@@ -780,7 +785,7 @@ class WC_Subscriptions_Upgrader {
public static function upgrade_in_progress_notice() {
wcs_deprecated_function( __METHOD__, 'subscriptions-core 7.7.0' );
include_once( dirname( __FILE__ ) . '/templates/wcs-upgrade-in-progress.php' );
include_once __DIR__ . '/templates/wcs-upgrade-in-progress.php';
WCS_Upgrade_Logger::add( 'Loaded database upgrade in progress notice...' );
}
@@ -827,7 +832,7 @@ class WC_Subscriptions_Upgrader {
$active_version = self::$stored_core_library_version;
$settings_page = admin_url( 'admin.php?page=wc-settings&tab=subscriptions' );
include_once( dirname( __FILE__ ) . '/templates/wcs-about.php' );
include_once __DIR__ . '/templates/wcs-about.php';
}
/**
@@ -1031,16 +1036,20 @@ class WC_Subscriptions_Upgrader {
sprintf(
// translators: 1-2: opening/closing <strong> tags, 3-4: opening/closing tags linked to ticket form.
esc_html__( '%1$sWarning!%2$s We discovered an issue in %1$sWooCommerce Subscriptions 2.3.0 - 2.3.2%2$s that may cause your subscription renewal order and customer subscription caches to contain invalid data. For information about how to update the cached data, please %3$sopen a new support ticket%4$s.', 'woocommerce-subscriptions' ),
'<strong>', '</strong>',
'<a href="https://woocommerce.com/my-account/marketplace-ticket-form/" target="_blank">', '</a>'
'<strong>',
'</strong>',
'<a href="https://woocommerce.com/my-account/marketplace-ticket-form/" target="_blank">',
'</a>'
)
);
$admin_notice->set_actions( array(
$admin_notice->set_actions(
array(
'name' => 'Dismiss',
'url' => wp_nonce_url( add_query_arg( $action, 'dismiss' ), $action, $nonce ),
),
) );
array(
'name' => 'Dismiss',
'url' => wp_nonce_url( add_query_arg( $action, 'dismiss' ), $action, $nonce ),
),
)
);
$admin_notice->display();
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Upgrade script for version 7.8.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Plugin_Upgrade_7_8_0 {
/**
* Check if the Gifting plugin is enabled and update the settings.
*
* @since 7.8.0
*/
public static function check_gifting_plugin_is_enabled() {
WCS_Upgrade_Logger::add( 'Checking if the Gifting plugin is enabled...' );
if ( ! is_plugin_active( 'woocommerce-subscriptions-gifting/woocommerce-subscriptions-gifting.php' ) ) {
WCS_Upgrade_Logger::add( 'Gifting plugin is not enabled, skipping...' );
return;
}
WCS_Upgrade_Logger::add( 'Gifting plugin is enabled, updating Gifting settings...' );
update_option( 'woocommerce_subscriptions_gifting_enable_gifting', 'yes' );
update_option( 'woocommerce_subscriptions_gifting_default_option', 'enabled' );
}
}

View File

@@ -0,0 +1,864 @@
<?php
/**
* Sets up and manages subscription gifting functionality.
*/
class WCS_Gifting {
/**
* Plugin's current version.
*
* @var string
*/
public static $version = '2.9.0'; // WRCS: DEFINED_VERSION.
/**
* Minimum WooCommerce version required.
*
* @var string
*/
public static $wc_minimum_supported_version = '3.0';
/**
* Minimum WooCommerce Subscription version required.
*
* @var string
*/
public static $wcs_minimum_supported_version = '2.2';
/**
* Minimum WooCommerce Memberships version required for integration.
*
* @var string
*/
public static $wcm_minimum_supported_version = '1.4';
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_action( 'wp_enqueue_scripts', __CLASS__ . '::gifting_scripts' );
// Needs to run after Subscriptions has loaded its dependant classes.
self::load_dependant_classes();
add_action( 'woocommerce_subscription_before_actions', __CLASS__ . '::add_billing_period_table_row' );
add_filter( 'woocommerce_get_formatted_subscription_total', __CLASS__ . '::get_formatted_recipient_total', 10, 2 );
if ( ! class_exists( 'WC_Subscriptions_Data_Copier' ) ) {
add_filter( 'wcs_renewal_order_meta_query', __CLASS__ . '::remove_renewal_order_meta_query', 11 );
} else {
add_filter( 'wc_subscriptions_renewal_order_data', __CLASS__ . '::remove_renewal_order_meta', 11 );
}
// Handle "_is_gifted_subscription" argument in wc_get_orders().
add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', array( __CLASS__, 'handle_is_gifted_subscription_query_var' ), 10, 2 );
}
/**
* Don't carry the _recipient_user meta data to renewal orders.
*
* @param array $order_meta Renewal order meta.
*
* @return array
*/
public static function remove_renewal_order_meta( $order_meta ) {
unset( $order_meta['_recipient_user'] );
return $order_meta;
}
/**
* Don't carry recipient meta data to renewal orders.
*
* @param string $order_meta_query Renewal order meta-query.
*/
public static function remove_renewal_order_meta_query( $order_meta_query ) {
$order_meta_query .= " AND `meta_key` NOT IN ('_recipient_user')";
return $order_meta_query;
}
/**
* Loads classes after plugins for classes dependant on other plugin files.
*/
public static function load_dependant_classes() {
require_once 'class-wcsg-query.php';
if ( function_exists( 'wc_memberships' ) ) {
if ( version_compare( get_option( 'wc_memberships_version' ), self::$wcm_minimum_supported_version, '>=' ) ) {
require_once 'class-wcsg-memberships-integration.php';
} else {
add_action( 'admin_notices', 'WCS_Gifting::plugin_dependency_notices' );
}
}
}
/**
* Register/queue frontend scripts.
*/
public static function gifting_scripts() {
wp_register_script( 'woocommerce_subscriptions_gifting', plugins_url( '/assets/js/gifting/wcs-gifting.js', WC_Subscriptions::$plugin_file ), array( 'jquery' ), WC_Subscriptions::$version, true );
wp_enqueue_script( 'woocommerce_subscriptions_gifting' );
wp_enqueue_style(
'woocommerce_subscriptions_gifting',
plugins_url( '/assets/css/gifting/shortcode-checkout.css', WC_Subscriptions::$plugin_file ),
array( 'wp-components' ),
WC_VERSION,
'all'
);
}
/**
* Determines if an email address belongs to the current user.
*
* @param string $email Email address.
* @return bool Returns whether the email address belongs to the current user.
*/
public static function email_belongs_to_current_user( $email ) {
$emails_to_try = array();
if ( is_user_logged_in() ) {
/** @var WP_User $current_user */
$current_user = wp_get_current_user();
$emails_to_try[] = $current_user->user_email;
}
if ( is_checkout() && ! empty( $_POST['billing_email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.CSRF.NonceVerification.NoNonceVerification
$emails_to_try[] = sanitize_email( wp_unslash( $_POST['billing_email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.CSRF.NonceVerification.NoNonceVerification
}
return in_array( $email, $emails_to_try, true );
}
/**
* Validates an array of recipient emails scheduling error notices if an error is found.
*
* @param array $recipients An array of recipient email addresses.
* @return bool Returns whether any errors have occurred.
*/
public static function validate_recipient_emails( $recipients ) {
$invalid_email_found = false;
$self_gifting_found = false;
if ( is_array( $recipients ) ) {
foreach ( $recipients as $key => $recipient ) {
$cleaned_recipient = sanitize_email( $recipient );
if ( $recipient === $cleaned_recipient && is_email( $cleaned_recipient ) ) {
if ( ! $self_gifting_found && self::email_belongs_to_current_user( $cleaned_recipient ) ) {
wc_add_notice( __( 'Please enter someone else\'s email address.', 'woocommerce-subscriptions' ), 'error' );
$self_gifting_found = true;
}
} elseif ( ! empty( $recipient ) && ! $invalid_email_found ) {
wc_add_notice( __( ' Invalid email address.', 'woocommerce-subscriptions' ), 'error' );
$invalid_email_found = true;
}
}
}
return ! ( $invalid_email_found || $self_gifting_found );
}
/**
* Attaches recipient information to a subscription cart item.
*
* @param object $item The item in the cart to be updated.
* @param string $key Cart item key.
* @param array $new_recipient_data The new recipient information for the item.
*/
public static function update_cart_item_recipient( $item, $key, $new_recipient_data ) {
if ( empty( $item['wcsg_gift_recipients_email'] ) || $item['wcsg_gift_recipients_email'] != $new_recipient_data ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
WC()->cart->cart_contents[ $key ]['wcsg_gift_recipients_email'] = $new_recipient_data;
}
}
/**
* Populates the cart item data that will be used by WooCommerce to generate a unique ID for the cart item. That is to
* avoid merging different products when they aren't the same. Previously the resubscribe status was ignored.
*
* @param array $item A cart item with all its data.
* @param string $key A cart item key.
* @param array $new_recipient_data Email address of the new recipient.
* @return array New cart item data.
*/
private static function add_cart_item_data( $item, $key, $new_recipient_data ) {
// start with a clean slate.
$cart_item_data = array();
// Add the recipient email.
if ( ! empty( $new_recipient_data ) ) {
$cart_item_data = array( 'wcsg_gift_recipients_email' => $new_recipient_data );
}
// Add resubscribe data.
if ( array_key_exists( 'subscription_resubscribe', $item ) ) {
$cart_item_data = array_merge( $cart_item_data, array( 'subscription_resubscribe' => $item['subscription_resubscribe'] ) );
}
$cart_item_data = apply_filters( 'wcsg_cart_item_data', $cart_item_data, $item, $key, $new_recipient_data );
return $cart_item_data;
}
/**
* Checks on each admin page load if Gifting plugin is activated.
*
* Apparently the official WP API is "lame" and it's far better to use an upgrade routine fired on admin_init: https://core.trac.wordpress.org/ticket/14170#comment:68
*
* @deprecated This is a hangover from the time when Subscriptions Gifting was a separate plugin.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.1.
*/
public static function maybe_activate() {
wcs_deprecated_function( __METHOD__, '7.8.0' );
}
/**
* Called when the plugin is deactivated. Deletes the is active flag and fires an action.
*
* @deprecated This is a hangover from the time when Subscriptions Gifting was a separate plugin.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function deactivate() {
wcs_deprecated_function( __METHOD__, '7.8.0' );
}
/**
* Renders the add recipient fields (including the checkbox and e-mail input).
*
* @param string $email E-mail address.
* @param string $id ID, for uniqueness on page.
* @param string $print_or_return Wether to print or return the HTML content. Optional. Default behaviour is to print the string. Pass 'return' to return the HTML content instead.
* @return string Returns the HTML string if $print_or_return is set to 'return', otherwise prints the HTML and nothing is returned.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.
*/
public static function render_add_recipient_fields( $email = '', $id = '', $print_or_return = 'print' ) {
$output = wc_get_template_html(
'html-add-recipient.php',
self::get_add_recipient_template_args( $email, $id ),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
);
if ( 'return' === $print_or_return ) {
return $output;
} else {
echo wp_kses(
$output,
array(
'fieldset' => array(),
'input' => array(
'type' => array(),
'id' => array(),
'class' => array(),
'style' => array(),
'value' => array(),
'checked' => array(),
'disabled' => array(),
'data-recipient' => array(),
'name' => array(),
'placeholder' => array(),
'aria-label' => array(),
),
'label' => array(
'for' => array(),
),
'div' => array(
'class' => array(),
'style' => array(),
),
'p' => array(
'class' => array(),
'style' => array(),
'id' => array(),
),
'svg' => array(
'xmlns' => array(),
'viewbox' => array(),
'width' => array(),
'height' => array(),
'aria-hidden' => array(),
'focusable' => array(),
),
'path' => array(
'd' => array(),
),
'span' => array(),
)
);
}
return '';
}
/**
* Build the set of arguments to be passed to the "Add Recipient" template.
*
* @param string $email E-mail address.
* @param string $id ID, for CSS uniqueness on page.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.
*/
public static function get_add_recipient_template_args( $email = '', $id = '' ) {
$id = $id ? esc_attr( $id ) : '0';
// E-mail field.
$email_field_args = array(
'placeholder' => __( 'Recipient\'s Email Address', 'woocommerce-subscriptions' ),
'class' => array( 'woocommerce_subscriptions_gifting_recipient_email' ),
'style_attributes' => array(),
);
if ( ! empty( $email ) && ( self::email_belongs_to_current_user( $email ) || ! is_email( $email ) ) ) {
array_push( $email_field_args['class'], 'woocommerce-invalid' );
}
// "This is a gift" checkbox.
$checkbox_field_args = array(
'class' => apply_filters( 'wcsg_recipient_checkbox_class', array() ),
'style_attributes' => apply_filters( 'wcsg_recipient_checkbox_style_attributes', array() ),
'disabled' => apply_filters( 'wcsg_recipient_checkbox_disabled', false ),
'checked' => empty( $email ) ? apply_filters( 'wcsg_recipient_checkbox_checked', false ) : true,
);
$nonce_field = '<input type="hidden" id="_wcsgnonce_' . $id . '" name="_wcsgnonce" value="' . wp_create_nonce( 'wcsg_add_recipient' ) . '" />';
$nonce_field .= wp_referer_field( false );
$args = array(
'email' => $email,
'id' => $id,
'container_style_attributes' => apply_filters( 'wcsg_recipient_fields_style_attributes', empty( $email ) ? array( 'display: none;' ) : array(), $email ),
'container_css_class' => apply_filters( 'wcsg_recipient_fields_css_class', array(), $email ),
'email_field_args' => apply_filters( 'wcsg_recipient_email_field_args', $email_field_args, $email ),
'checkbox_field_args' => apply_filters( 'wcsg_recipient_checkbox_field_args', $checkbox_field_args, $email ),
'nonce_field' => $nonce_field,
);
return $args;
}
/**
* Adds row to subscription details table that displays subscription period for recipients.
*
* @param WC_Subscription $subscription Subscription object.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function add_billing_period_table_row( $subscription ) {
if ( ! wcsg_is_wc_subscriptions_pre( '2.2.19' ) && self::is_gifted_subscription( $subscription ) && get_current_user_id() == self::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$subscription_details = array(
'recurring_amount' => '',
'subscription_period' => $subscription->get_billing_period(),
'subscription_interval' => $subscription->get_billing_interval(),
'initial_amount' => '',
'use_per_slash' => false,
);
$billing_period_string = apply_filters( 'woocommerce_subscription_price_string_details', $subscription_details, $subscription );
?>
<tr>
<td><?php echo esc_html_x( 'Renewing', 'table heading', 'woocommerce-subscriptions' ); ?></td>
<td><?php echo esc_html( wcs_price_string( $billing_period_string ) ); ?></td>
</tr>
<?php
}
}
/**
* Reformats the price of the subscription to hide it if the user is the recipient.
*
* @param string $formatted_order_total The order total formatted.
* @param WC_Subscription $subscription Subscription object.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function get_formatted_recipient_total( $formatted_order_total, $subscription ) {
global $wp;
if ( ! wcsg_is_wc_subscriptions_pre( '2.2.19' ) && is_account_page() && isset( $wp->query_vars['subscriptions'] ) && self::is_gifted_subscription( $subscription ) && get_current_user_id() == self::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$formatted_order_total = '-';
}
return $formatted_order_total;
}
/**
* Returns a combination of the customer's first name, last name and email depending on what the customer has set.
*
* @param int $user_id The ID of the customer user.
* @param bool $strip_tags Whether to strip HTML tags in user name (defaulted to false).
*/
public static function get_user_display_name( $user_id, $strip_tags = false ) {
$user = get_user_by( 'id', $user_id );
$name = '';
if ( ! empty( $user->first_name ) ) {
$name = $user->first_name . ( ( ! empty( $user->last_name ) ) ? ' ' . $user->last_name : '' ) . ' (' . make_clickable( $user->user_email ) . ')';
} else {
$name = make_clickable( $user->user_email );
}
if ( $strip_tags ) {
$name = wp_strip_all_tags( $name );
}
return $name;
}
/**
* Displays plugin dependency notices if required plugins are inactive or the installed version is less than a
* supported version.
*/
public static function plugin_dependency_notices() {
if ( ! class_exists( 'WooCommerce' ) ) {
self::output_plugin_dependency_notice( 'WooCommerce' );
return;
}
if ( ! class_exists( 'WC_Subscription' ) ) {
self::output_plugin_dependency_notice( 'WooCommerce Subscriptions / WooCommerce Payments' );
return;
}
if ( version_compare( get_option( 'woocommerce_subscriptions_active_version' ), self::$wcs_minimum_supported_version, '<' ) ) {
self::output_plugin_dependency_notice( 'WooCommerce Subscriptions', self::$wcs_minimum_supported_version );
}
if ( version_compare( get_option( 'woocommerce_db_version' ), self::$wc_minimum_supported_version, '<' ) ) {
self::output_plugin_dependency_notice( 'WooCommerce', self::$wc_minimum_supported_version );
}
if ( class_exists( 'WC_Memberships' ) && version_compare( get_option( 'wc_memberships_version' ), self::$wcm_minimum_supported_version, '<' ) ) {
self::output_plugin_dependency_notice( 'WooCommerce Memberships', self::$wcm_minimum_supported_version );
}
}
/**
* Prints a plugin dependency admin notice. If a required version is supplied an invalid version notice is printed,
* otherwise an inactive plugin notice is printed.
*
* @param string $plugin_name The plugin name.
* @param string|bool $required_version The minimum supported version of the plugin.
*/
public static function output_plugin_dependency_notice( $plugin_name, $required_version = false ) {
if ( current_user_can( 'activate_plugins' ) ) {
if ( $required_version ) {
?>
<div id="message" class="error">
<p>
<?php
if ( 'WooCommerce Memberships' === $plugin_name ) {
// translators: 1$-2$: opening and closing <strong> tags, 3$ plugin name, 4$ required plugin version, 5$-6$: opening and closing link tags, leads to plugins.php in admin, 7$: line break, 8$-9$ Opening and closing small tags.
printf( esc_html__( '%1$sWooCommerce Subscriptions Gifting Membership integration is inactive.%2$s In order to integrate with WooCommerce Memberships, WooCommerce Subscriptions Gifting requires %3$s %4$s or newer. %5$sPlease update &raquo;%6$s %7$s%8$sNote: All other WooCommerce Subscriptions Gifting features will remain available, however purchasing membership plans for recipients will fail to grant the membership to the gift recipient.%9$s', 'woocommerce-subscriptions' ), '<strong>', '</strong>', esc_html( $plugin_name ), esc_html( $required_version ), '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>', '</br>', '<small>', '</small>' );
} else {
// translators: 1$-2$: opening and closing <strong> tags, 3$ plugin name, 4$ required plugin version, 5$-6$: opening and closing link tags, leads to plugins.php in admin.
printf( esc_html__( '%1$sWooCommerce Subscriptions Gifting is inactive.%2$s This version of WooCommerce Subscriptions Gifting requires %3$s %4$s or newer. %5$sPlease update &raquo;%6$s', 'woocommerce-subscriptions' ), '<strong>', '</strong>', esc_html( $plugin_name ), esc_html( $required_version ), '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' );
}
?>
</p>
</div>
<?php
} else {
$message = null;
if ( 'WooCommerce Subscriptions / WooCommerce Payments' === $plugin_name ) {
$wcs_plugin_url = 'http://www.woocommerce.com/products/woocommerce-subscriptions/';
$wcpay_plugin_url = 'http://www.woocommerce.com/products/woocommerce-payments/';
// translators: 1$-2$: opening and closing <strong> tags, 3$:opening link tag, leads to WooCommerce Payments plugin product page, 4$:opening link tag, leads to WooCommerce Subscriptions plugin product page, 5$-6$: opening and closing link tags, leads to plugins.php in admin.
$message = sprintf( esc_html__( '%1$sWooCommerce Subscriptions Gifting is inactive.%2$s WooCommerce Subscriptions Gifting requires either the %3$sWooCommerce Payments%6$s or %4$sWooCommerce Subscriptions%6$s plugin to be active to work correctly. Please %5$sinstall & activate either one &raquo;%6$s', 'woocommerce-subscriptions' ), '<strong>', '</strong>', '<a href="' . esc_url( $wcpay_plugin_url ) . '">', '<a href="' . esc_url( $wcs_plugin_url ) . '">', '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' );
} elseif ( 'WooCommerce' === $plugin_name ) {
$plugin_url = 'http://wordpress.org/extend/plugins/woocommerce/';
// translators: 1$-2$: opening and closing <strong> tags, 3$ plugin name, 4$:opening link tag, leads to plugin product page, 5$-6$: opening and closing link tags, leads to plugins.php in admin.
$message = sprintf( esc_html__( '%1$sWooCommerce Subscriptions Gifting is inactive.%2$s WooCommerce Subscriptions Gifting requires the %4$s%3$s%6$s plugin to be active to work correctly. Please %5$sinstall & activate %3$s &raquo;%6$s', 'woocommerce-subscriptions' ), '<strong>', '</strong>', esc_html( $plugin_name ), '<a href="' . esc_url( $plugin_url ) . '">', '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' );
}
if ( $message ) {
?>
<div id="message" class="error">
<p>
<?php
echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
</p>
</div>
<?php
}
}
}
}
/**
* Checks whether a subscription is a gifted subscription.
*
* @param int|WC_Subscription $subscription either a subscription object or subscription's ID.
* @return bool
*/
public static function is_gifted_subscription( $subscription ) {
$is_gifted_subscription = false;
if ( ! $subscription instanceof WC_Subscription ) {
$subscription = wcs_get_subscription( $subscription );
}
if ( wcs_is_subscription( $subscription ) ) {
$recipient_user_id = self::get_recipient_user( $subscription );
$has_recipient_email = $subscription->get_meta( '_recipient_user_email_address' );
$is_gifted_subscription = ( ! empty( $recipient_user_id ) && is_numeric( $recipient_user_id ) ) || $has_recipient_email;
}
return $is_gifted_subscription;
}
/**
* Returns a list of all order item ids and their containing order ids that have been purchased for a recipient.
*
* @param int $recipient_user_id User ID.
* @return array
*/
public static function get_recipient_order_items( $recipient_user_id ) {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT o.order_id, i.order_item_id
FROM {$wpdb->prefix}woocommerce_order_itemmeta AS i
INNER JOIN {$wpdb->prefix}woocommerce_order_items as o
ON i.order_item_id=o.order_item_id
WHERE meta_key = 'wcsg_recipient'
AND meta_value = %s",
'wcsg_recipient_id_' . $recipient_user_id
),
ARRAY_A
);
}
/**
* Returns the user's shipping address.
*
* @param int $user_id User ID.
* @return array
*/
public static function get_users_shipping_address( $user_id ) {
return array(
'first_name' => get_user_meta( $user_id, 'shipping_first_name', true ),
'last_name' => get_user_meta( $user_id, 'shipping_last_name', true ),
'company' => get_user_meta( $user_id, 'shipping_company', true ),
'address_1' => get_user_meta( $user_id, 'shipping_address_1', true ),
'address_2' => get_user_meta( $user_id, 'shipping_address_2', true ),
'city' => get_user_meta( $user_id, 'shipping_city', true ),
'state' => get_user_meta( $user_id, 'shipping_state', true ),
'postcode' => get_user_meta( $user_id, 'shipping_postcode', true ),
'country' => get_user_meta( $user_id, 'shipping_country', true ),
);
}
/**
* Determines if an order contains a gifted subscription.
*
* @param mixed $order the order id or order object to check.
* @return bool
*/
public static function order_contains_gifted_subscription( $order ) {
if ( ! is_object( $order ) ) {
$order = wc_get_order( $order );
}
if ( ! is_a( $order, 'WC_Order' ) ) {
return false;
}
$contains_gifted_subscription = false;
foreach ( wcs_get_subscriptions_for_order( $order ) as $subscription_id => $subscription ) {
if ( self::is_gifted_subscription( $subscription ) ) {
$contains_gifted_subscription = true;
break;
}
}
return $contains_gifted_subscription;
}
/**
* Retrieves the user id of the recipient stored in order item meta.
*
* @param mixed $order_item the order item to check.
* @return mixed bool|int The recipient user id or false if the order item is not gifted.
*/
public static function get_order_item_recipient_user_id( $order_item ) {
if ( is_a( $order_item, 'WC_Order_Item' ) && $order_item->meta_exists( 'wcsg_recipient' ) ) {
$raw_recipient_meta = $order_item->get_meta( 'wcsg_recipient' );
} elseif ( isset( $order_item['item_meta']['wcsg_recipient'] ) ) {
$raw_recipient_meta = $order_item['item_meta']['wcsg_recipient'][0];
}
return isset( $raw_recipient_meta ) ? substr( $raw_recipient_meta, strlen( 'wcsg_recipient_id_' ) ) : false;
}
/**
* Create a recipient user account.
*
* @param string $recipient_email Recipient's e-mail address.
* @return int ID for newly created user.
*/
public static function create_recipient_user( $recipient_email ) {
$username = explode( '@', $recipient_email );
$username = sanitize_user( $username[0], true );
$counter = 1;
$original_username = $username;
while ( username_exists( $username ) ) {
$username = $original_username . $counter;
++$counter;
}
$password = wp_generate_password();
$recipient_user_id = wc_create_new_customer( $recipient_email, $username, $password );
// set a flag to force the user to update/set account information on login.
update_user_meta( $recipient_user_id, 'wcsg_update_account', 'true' );
return $recipient_user_id;
}
/**
* Retrieve the recipient user ID from a subscription.
*
* @param WC_Subscription $subscription Subscription object.
* @return string The recipient's user ID. Returns an empty string if there is no recipient set.
*/
public static function get_recipient_user( $subscription ) {
$recipient_user_id = '';
if ( method_exists( $subscription, 'get_meta' ) ) {
if ( $subscription->meta_exists( '_recipient_user' ) ) {
$recipient_user_id = $subscription->get_meta( '_recipient_user' );
}
} else { // WC < 3.0.
$recipient_user_id = $subscription->recipient_user;
}
return $recipient_user_id;
}
/**
* Set the recipient user ID on a subscription.
*
* @param WC_Subscription $subscription Subscription object.
* @param int $user_id The user ID of the user to set as the recipient on the subscription.
* @param string $save Whether to save the data or not, 'save' to save the data, otherwise it won't be saved.
* @param int $meta_id The meta ID of existing meta data if you wish to overwrite an existing recipient meta value.
*/
public static function set_recipient_user( &$subscription, $user_id, $save = 'save', $meta_id = 0 ) {
$current_user_id = absint( self::get_recipient_user( $subscription ) );
$subscription->recipient_user = $user_id;
if ( 'save' === $save ) {
$subscription->update_meta_data( '_recipient_user', $user_id, $meta_id );
$subscription->save();
$gifting_subscription_items = $subscription->get_items();
$gifting_subcription_item = reset( $gifting_subscription_items );
if ( ! empty( $gifting_subcription_item ) ) {
$order = wc_get_order( $subscription->get_parent_id() );
foreach ( $order->get_items() as $order_item ) {
if ( $order_item->get_meta( '_wcsg_cart_key' ) === $gifting_subcription_item->get_meta( '_wcsg_cart_key' ) ) {
$order_item->add_meta_data( 'wcsg_recipient', 'wcsg_recipient_id_' . $user_id, true );
$order_item->save();
}
}
}
do_action( 'woocommerce_subscriptions_gifting_recipient_changed', $subscription, $user_id, $current_user_id );
}
}
/**
* Delete the recipient user ID on a subscription
*
* @param WC_Subscription $subscription Subscription object.
* @param string $save Whether to save the data or not, 'save' to save the data, otherwise it won't be saved.
* @param int $meta_id The meta ID of existing recipient meta data if you wish to only delete a field specified by ID.
*/
public static function delete_recipient_user( &$subscription, $save = 'save', $meta_id = 0 ) {
unset( $subscription->recipient_user );
// Save the data.
if ( 'save' === $save ) {
if ( ! empty( $meta_id ) ) {
$subscription->delete_meta_data_by_mid( $meta_id );
} else {
$subscription->delete_meta_data( '_recipient_user' );
}
$subscription->save();
}
}
/**
* Retrieves a set of gifted subscriptions based on certain parameters.
*
* @see wc_get_orders()
*
* @param array $args Custom args for query, excluding 'type' and custom var 'is_gifted_subscription'.
*
* @return WC_Order[]
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.3.
*/
public static function get_gifted_subscriptions( $args = array() ) {
$query_args = wp_parse_args(
$args,
array(
'limit' => -1,
'status' => 'any',
'orderby' => 'date',
'order' => 'desc',
)
);
$query_args['type'] = 'shop_subscription';
if ( function_exists( 'wcs_get_orders_with_meta_query' ) ) {
$query_args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_recipient_user',
'compare' => 'EXISTS',
),
);
return wcs_get_orders_with_meta_query( $query_args );
}
$query_args['is_gifted_subscription'] = true;
return wc_get_orders( $query_args );
}
/**
* Handle custom WCS Gifting query vars to get subscriptions with 'WCS Gifting' meta.
*
* @param array $query Args for WP_Query.
* @param array $query_vars Query vars from WC_Order_Query.
*
* @return array modified $query
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.3.
*/
public static function handle_is_gifted_subscription_query_var( $query, $query_vars ) {
if ( ! empty( $query_vars['is_gifted_subscription'] ) && true === $query_vars['is_gifted_subscription'] ) {
$query['meta_query'][] = array(
array(
'key' => '_recipient_user',
'compare' => 'EXISTS',
),
);
}
return $query;
}
/**
* Does the site requires shipping address data for non-virtual products. Default: true
*
* @return bool
*/
public static function require_shipping_address_for_virtual_products() {
return apply_filters( 'wcsg_require_shipping_address_for_virtual_products', true );
}
/**
* Counts the number of Gifted Subscriptions.
*
* @return int
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
public static function get_gifted_subscriptions_count() {
global $wpdb;
if ( function_exists( 'wcs_is_custom_order_tables_usage_enabled' ) && wcs_is_custom_order_tables_usage_enabled() ) {
$count = $wpdb->get_var(
"
SELECT COUNT(DISTINCT o.id) FROM {$wpdb->prefix}wc_orders o
INNER JOIN {$wpdb->prefix}wc_orders_meta om ON (o.id = om.order_id)
WHERE o.type = 'shop_subscription'
AND o.status NOT IN ( 'auto-draft', 'trash' )
AND om.meta_key = '_recipient_user'
"
);
} else {
$count = $wpdb->get_var(
"
SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON (p.ID = pm.post_id)
WHERE p.post_type = 'shop_subscription'
AND p.post_status NOT IN ( 'auto-draft', 'trash' )
AND pm.meta_key = '_recipient_user'
"
);
}
return absint( $count );
}
/**
* Register/queue admin scripts.
*/
public static function admin_scripts() {
_deprecated_function( __METHOD__, '2.0.0', 'WCSG_Admin::enqueue_scripts()' );
}
/**
* Install wcsg
*/
public static function wcsg_install() {
_deprecated_function( __METHOD__, '2.0.0', 'WCS_Gifting::maybe_activate()' );
}
/**
* Flush rewrite rules if they haven't been flushed since plugin activation
*/
public static function maybe_flush_rewrite_rules() {
_deprecated_function( __METHOD__, '2.0.0', 'flush_rewrite_rules()' );
}
/**
* Overrides the default recent order template for gifted subscriptions
*
* @param string $located Path to template.
* @param string $template_name Template name.
* @param array $args Arguments.
*/
public static function get_recent_orders_template( $located, $template_name, $args ) {
_deprecated_function( __FUNCTION__, '2.0.0', 'WCSG_Template_Loader::get_recent_orders_template()' );
WCSG_Template_Loader::get_recent_orders_template( $located, $template_name, $args );
}
/**
* Generates an array of arguments used to create the recipient email html fields.
*
* @param string $email E-mail address.
* @return array email_field_args A set of html attributes
* @deprecated 2.1
*/
public static function get_recipient_email_field_args( $email ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Gifting::get_add_recipient_template_args()' );
$args = self::get_add_recipient_template_args( $email );
return $args['email_field_args'];
}
/**
* Generates an array of arguments used to create the recipient checkbox html fields
*
* @param string $email The email of the gift recipient.
* @return array checkbox_field_args A set of html attributes
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
* @deprecated 2.1
*/
public static function get_recipient_checkbox_field_args( $email ) {
_deprecated_function( __METHOD__, '2.1', 'WCS_Gifting::get_add_recipient_template_args()' );
$args = self::get_add_recipient_template_args( $email );
return $args['checkbox_field_args'];
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* Handles System report functionality
*
* @package WooCommerce Subscriptions Gifting
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
defined( 'ABSPATH' ) || exit;
/**
* System Status Class
*/
class WCSG_Admin_System_Status {
/**
* Array of Gifting information for display on the System Status page.
*
* @var array
*/
private static $gifting_data = array();
/**
* Hooks.
*/
public static function init() {
add_filter( 'woocommerce_system_status_report', array( __CLASS__, 'render_system_status_items' ) );
}
/**
* Renders Gifting system status report.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
public static function render_system_status_items() {
self::set_gifting_information();
self::set_theme_overrides();
$system_status_sections = array(
array(
'title' => __( 'Subscriptions Gifting', 'woocommerce-subscriptions' ),
'tooltip' => __( 'This section shows any information about Subscriptions Gifting.', 'woocommerce-subscriptions' ),
'data' => apply_filters( 'wcsg_system_status', self::$gifting_data ),
),
);
foreach ( $system_status_sections as $section ) {
$section_title = $section['title'];
$section_tooltip = $section['tooltip'];
$debug_data = $section['data'];
include plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/admin/status.php';
}
}
/**
* Sets the theme overrides area for Subscriptions Gifting.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
private static function set_theme_overrides() {
$theme_overrides = self::get_theme_overrides();
if ( ! empty( $theme_overrides['overrides'] ) ) {
self::$gifting_data['wcsg_theme_overrides'] = array(
'name' => _x( 'Subscriptions Gifting Template Theme Overrides', 'name for the system status page', 'woocommerce-subscriptions' ),
'label' => _x( 'Subscriptions Gifting Template Theme Overrides', 'label for the system status page', 'woocommerce-subscriptions' ),
'data' => $theme_overrides['overrides'],
);
// Include a note on how to update if the templates are out of date.
if ( ! empty( $theme_overrides['has_outdated_templates'] ) && true === $theme_overrides['has_outdated_templates'] ) {
self::$gifting_data['wcsg_theme_overrides'] += array(
'mark_icon' => 'warning',
/* Translators: 1) an <a> tag pointing to a doc on how to fix outdated templates, 2) closing </a> tag. */
'note' => sprintf( __( '%1$sLearn how to update%2$s', 'woocommerce-subscriptions' ), '<a href="https://docs.woocommerce.com/document/fix-outdated-templates-woocommerce/" target="_blank">', '</a>' ),
);
}
}
}
/**
* Determine which of our files have been overridden by the theme and if the theme files are outdated.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
* @return array
*/
private static function get_theme_overrides() {
$wcsg_template_dir = dirname( WC_Subscriptions::$plugin_file ) . '/templates/gifting/';
$wc_template_path = trailingslashit( wc()->template_path() );
$theme_root = trailingslashit( get_theme_root() );
$overridden = array();
$outdated = false;
$templates = WC_Admin_Status::scan_template_files( $wcsg_template_dir );
foreach ( $templates as $template ) {
$theme_file = false;
$locations = array(
get_stylesheet_directory() . "/{$template}",
get_stylesheet_directory() . "/{$wc_template_path}{$template}",
get_template_directory() . "/{$template}",
get_template_directory() . "/{$wc_template_path}{$template}",
);
foreach ( $locations as $location ) {
if ( is_readable( $location ) ) {
$theme_file = $location;
break;
}
}
if ( ! empty( $theme_file ) ) {
$core_version = WC_Admin_Status::get_file_version( $wcsg_template_dir . $template );
$theme_version = WC_Admin_Status::get_file_version( $theme_file );
$overridden_template_output = sprintf( '<code>%s</code>', esc_html( str_replace( $theme_root, '', $theme_file ) ) );
if ( $core_version && ( empty( $theme_version ) || version_compare( $theme_version, $core_version, '<' ) ) ) {
$outdated = true;
$overridden_template_output .= sprintf(
/* translators: %1$s is the file version, %2$s is the core version */
esc_html__( 'version %1$s is out of date. The core version is %2$s', 'woocommerce-subscriptions' ),
'<strong style="color:red">' . esc_html( $theme_version ) . '</strong>',
'<strong>' . esc_html( $core_version ) . '</strong>'
);
}
$overridden['overrides'][] = $overridden_template_output;
}
}
$overridden['has_outdated_templates'] = $outdated;
return $overridden;
}
/**
* Gets the number of Gifted Subscriptions and adds it to the system status.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
private static function set_gifting_information() {
$gifted_subscriptions_count = WCS_Gifting::get_gifted_subscriptions_count();
self::$gifting_data['wcsg_gifted_subscriptions_count'] = array(
'name' => _x( 'Gifted Subscriptions Count', 'name for the system status page', 'woocommerce-subscriptions' ),
'label' => _x( 'Gifted Subscriptions Count', 'label for the system status page', 'woocommerce-subscriptions' ),
'data' => array( $gifted_subscriptions_count ),
);
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Gifting Admin Announcement Handler Class
*
* @package WooCommerce Subscriptions
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCSG_Admin_Welcome_Announcement {
/**
* Initialize the tour handler
*/
public static function init() {
// Register scripts and styles
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
// Add the tour HTML to the admin footer
add_action( 'admin_footer', array( __CLASS__, 'output_tour' ) );
}
/**
* Enqueue required scripts and styles
*/
public static function enqueue_scripts() {
$screen = get_current_screen();
// Only load on WooCommerce admin pages
if ( ! $screen || 'woocommerce_page_wc-admin' !== $screen->id ) {
return;
}
wp_enqueue_script(
'wcs-gifting-welcome-announcement',
plugins_url( '/build/gifting-welcome-announcement.js', WC_Subscriptions::$plugin_file ),
array( 'wp-components', 'wp-i18n' ),
WC_Subscriptions::$version,
true
);
wp_localize_script(
'wcs-gifting-welcome-announcement',
'wcsGiftingSettings',
array(
'imagesPath' => plugins_url( '/assets/images', WC_Subscriptions::$plugin_file ),
'pluginsUrl' => admin_url( 'plugins.php' ),
'subscriptionsUrl' => WC_Subscriptions_Admin::settings_tab_url() . '#woocommerce_subscriptions_gifting_enable_gifting',
'isStandaloneGiftingEnabled' => is_plugin_active( 'woocommerce-subscriptions-gifting/woocommerce-subscriptions-gifting.php' ),
)
);
wp_enqueue_style( 'wcs-gifting-welcome-announcement', plugins_url( '/build/style-gifting-welcome-announcement.css', WC_Subscriptions::$plugin_file ), array(), WC_Subscriptions::$version );
wp_set_script_translations(
'wcs-gifting-welcome-announcement',
'woocommerce-subscriptions',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'languages'
);
}
/**
* Output the tour HTML in the admin footer
*/
public static function output_tour() {
$screen = get_current_screen();
// Only load on WooCommerce admin pages
if ( ! $screen || 'woocommerce_page_wc-admin' !== $screen->id ) {
return;
}
// Add a div for the tour to be rendered into
echo '<div id="wcs-gifting-welcome-announcement-root" class="woocommerce-tour-kit"></div>';
}
/**
* Checks if the welcome tour has been dismissed.
*
* @return bool
*/
public static function is_welcome_announcement_dismissed() {
return '1' === get_option(
'woocommerce_subscriptions_gifting_is_welcome_announcement_dismissed',
''
);
}
}

View File

@@ -0,0 +1,564 @@
<?php
/**
* Admin integration.
*
* @package WooCommerce Subscriptions Gifting/Admin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class for admin-side integration.
*/
class WCSG_Admin {
/**
* Prefix used in all Gifting settings names.
*
* @var string
*/
public static $option_prefix = 'woocommerce_subscriptions_gifting';
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
add_filter( 'woocommerce_subscription_list_table_column_content', __CLASS__ . '::display_recipient_name_in_subscription_title', 1, 3 );
add_filter( 'woocommerce_order_items_meta_get_formatted', __CLASS__ . '::remove_recipient_order_item_meta', 1, 1 );
add_filter( 'woocommerce_subscription_settings', __CLASS__ . '::add_settings', 10, 1 );
if ( wcsg_is_wc_subscriptions_pre( '2.3.5' ) ) {
add_filter( 'request', __CLASS__ . '::request_query', 11, 1 );
} else {
add_filter( 'wcs_admin_request_query_subscriptions_for_customer', array( __CLASS__, 'request_query_customer_filter' ), 10, 2 );
}
add_action( 'woocommerce_admin_order_data_after_order_details', __CLASS__ . '::display_edit_subscription_recipient_field', 10, 1 );
// Save recipient user after WC have saved all subscription order items (40).
add_action( 'woocommerce_process_shop_order_meta', __CLASS__ . '::save_subscription_recipient_meta', 50, 2 );
add_action( 'admin_notices', __CLASS__ . '::admin_installed_notice' );
// Filter for gifted subscriptions.
add_action( 'restrict_manage_posts', array( __CLASS__, 'add_gifted_subscriptions_filter' ), 50 );
add_action( 'woocommerce_order_list_table_restrict_manage_orders', array( __CLASS__, 'add_gifted_subscriptions_filter' ), 50 );
add_action( 'pre_get_posts', array( __CLASS__, 'maybe_filter_by_gifted_subscriptions' ) );
add_action( 'woocommerce_shop_subscription_list_table_prepare_items_query_args', array( __CLASS__, 'filter_subscription_list_table_by_gifted_subscriptions' ) );
// Add "Resend new recipient account email".
add_filter( 'woocommerce_order_actions', array( __CLASS__, 'add_resend_new_recipient_account_email_action' ), 10, 1 );
add_action( 'woocommerce_order_action_wcsg_resend_new_recipient_account_email', array( __CLASS__, 'resend_new_recipient_account_email' ), 10, 1 );
add_filter( 'woocommerce_hidden_order_itemmeta', array( __CLASS__, 'hide_wcsg_recipient_meta' ) );
}
/**
* Hides the wcsg_recipient meta from the order item meta in the edit order page.
*
* @param array $hidden_order_itemmeta The hidden order item meta.
* @return array The hidden order item meta.
*/
public static function hide_wcsg_recipient_meta( $hidden_order_itemmeta ) {
$hidden_order_itemmeta[] = '_wcsg_cart_key';
return $hidden_order_itemmeta;
}
/**
* Register/queue admin scripts.
*/
public static function enqueue_scripts() {
global $post;
$screen = get_current_screen();
if ( 'shop_subscription' === $screen->id && WCS_Gifting::is_gifted_subscription( $post->ID ) ) {
wp_register_script( 'wcs_gifting_admin', plugins_url( '/assets/js/gifting/wcsg-admin.js', WC_Subscriptions::$plugin_file ), array( 'jquery', 'wc-admin-order-meta-boxes' ), WC_Subscriptions::$version, true );
wp_localize_script(
'wcs_gifting_admin',
'wcs_gifting',
array(
'revoke_download_permission_nonce' => wp_create_nonce( 'revoke_download_permission' ),
'ajax_url' => admin_url( 'admin-ajax.php' ),
)
);
wp_enqueue_script( 'wcs_gifting_admin' );
}
if ( true == get_transient( 'wcsg_show_activation_notice' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ), array(), WC_VERSION );
}
}
/**
* Formats the subscription title in the admin subscriptions table to include the recipient's name.
*
* @param string $column_content The column content HTML elements.
* @param WC_Subscription $subscription Subscription object.
* @param string $column The column name being rendered.
*/
public static function display_recipient_name_in_subscription_title( $column_content, $subscription, $column ) {
if ( 'order_title' === $column && WCS_Gifting::is_gifted_subscription( $subscription ) ) {
$recipient_id = WCS_Gifting::get_recipient_user( $subscription );
$recipient_user = get_userdata( $recipient_id );
$recipient_name = '<a href="' . esc_url( get_edit_user_link( $recipient_id ) ) . '">';
if ( ! empty( $recipient_user->first_name ) || ! empty( $recipient_user->last_name ) ) {
$recipient_name .= ucfirst( $recipient_user->first_name ) . ( ( ! empty( $recipient_user->last_name ) ) ? ' ' . ucfirst( $recipient_user->last_name ) : '' );
} else {
$recipient_name .= ucfirst( $recipient_user->display_name );
}
$recipient_name .= '</a>';
$purchaser_id = $subscription->get_user_id();
$purchaser_user = get_userdata( $purchaser_id );
$purchaser_name = '<a href="' . esc_url( get_edit_user_link( $purchaser_id ) ) . '">';
if ( ! empty( $purchaser_user->first_name ) || ! empty( $purchaser_user->last_name ) ) {
$purchaser_name .= ucfirst( $purchaser_user->first_name ) . ( ( ! empty( $purchaser_user->last_name ) ) ? ' ' . ucfirst( $purchaser_user->last_name ) : '' );
} else {
$purchaser_name .= ucfirst( $purchaser_user->display_name );
}
$purchaser_name .= '</a>';
// translators: $1: is subscription order number,$2: is recipient user's name, $3: is the purchaser user's name.
$column_content = sprintf( _x( '%1$s for %2$s purchased by %3$s', 'Subscription title on admin table. (e.g.: #211 for John Doe Purchased by: Jane Doe)', 'woocommerce-subscriptions' ), '<a href="' . esc_url( $subscription->get_edit_order_url() ) . '">#<strong>' . esc_attr( $subscription->get_order_number() ) . '</strong></a>', $recipient_name, $purchaser_name );
$column_content .= '</div>';
}
return $column_content;
}
/**
* Removes the recipient order item meta from the admin subscriptions table.
*
* @param array $formatted_meta formatted order item meta key, label and value.
*/
public static function remove_recipient_order_item_meta( $formatted_meta ) {
if ( is_admin() ) {
$screen = get_current_screen();
if ( isset( $screen->id ) && 'edit-shop_subscription' === $screen->id ) {
foreach ( $formatted_meta as $meta_id => $meta ) {
if ( 'wcsg_recipient' === $meta['key'] ) {
unset( $formatted_meta[ $meta_id ] );
}
}
}
}
return $formatted_meta;
}
/**
* Add Gifting specific settings to standard Subscriptions settings
*
* @param array $settings Current set of settings.
* @return array $settings New set of settings.
*/
public static function add_settings( $settings ) {
return array_merge(
$settings,
array(
array(
'name' => __( 'Gifting', 'woocommerce-subscriptions' ),
'type' => 'title',
'id' => self::$option_prefix,
),
array(
'name' => __( 'Enable gifting', 'woocommerce-subscriptions' ),
'desc' => __( 'Allow shoppers to gift a subscription', 'woocommerce-subscriptions' ),
'id' => self::$option_prefix . '_enable_gifting',
'default' => 'no',
'type' => 'checkbox',
'row_class' => 'enable-gifting',
),
array(
'name' => '',
'desc' => __( 'You can override this global setting on each product.', 'woocommerce-subscriptions' ),
'id' => self::$option_prefix . '_default_option',
'default' => 'disabled',
'type' => 'radio',
'desc_at_end' => true,
'row_class' => 'gifting-radios',
'options' => array(
'enabled' => __( 'Enabled for all products', 'woocommerce-subscriptions' ),
'disabled' => __( 'Disabled for all products', 'woocommerce-subscriptions' ),
),
),
array(
'name' => __( 'Gifting Checkbox Text', 'woocommerce-subscriptions' ),
'desc' => __( 'This is what shoppers will see in the product page and cart.', 'woocommerce-subscriptions' ),
'id' => self::$option_prefix . '_gifting_checkbox_text',
'default' => __( 'This is a gift', 'woocommerce-subscriptions' ),
'type' => 'text',
),
array(
'type' => 'sectionend',
'id' => self::$option_prefix,
),
)
);
}
/**
* Adds meta query to also include subscriptions the user is the recipient of when filtering subscriptions by customer.
* Compatibility method for Subscriptions < 2.3.5.
*
* @param array $vars Request vars.
* @return array
*/
public static function request_query( $vars ) {
global $typenow;
if ( 'shop_subscription' === $typenow ) {
// Add _recipient_user meta check when filtering by customer.
$user_id = isset( $_GET['_customer_user'] ) ? intval( $_GET['_customer_user'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $user_id ) {
$vars['meta_query'][] = array(
'key' => '_recipient_user',
'value' => $user_id,
'compare' => '=',
);
$vars['meta_query']['relation'] = 'OR';
}
}
return $vars;
}
/**
* Adds subscriptions the user is the recipient of when filtering subscriptions by customer on the backend.
*
* @param array $subscription_ids Current set of subscription IDs.
* @param int $customer_user_id User ID.
* @return array New set of subscription IDs.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
*/
public static function request_query_customer_filter( $subscription_ids, $customer_user_id ) {
$recipient_subscriptions = WCSG_Recipient_Management::get_recipient_subscriptions( $customer_user_id );
return array_merge( $subscription_ids, $recipient_subscriptions );
}
/**
* Output a recipient user select field in the edit subscription data metabox.
*
* @param WP_Post $subscription Subscription's post object.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
*/
public static function display_edit_subscription_recipient_field( $subscription ) {
if ( ! wcs_is_subscription( $subscription ) ) {
return;
} ?>
<p class="form-field form-field-wide wc-customer-user">
<label for="recipient_user"><?php esc_html_e( 'Recipient:', 'woocommerce-subscriptions' ); ?></label>
<?php
$user_string = '';
$user_id = '';
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
$user_id = WCS_Gifting::get_recipient_user( $subscription );
$user = get_user_by( 'id', $user_id );
$user_string = esc_html( $user->display_name ) . ' (#' . absint( $user->ID ) . ' &ndash; ' . esc_html( $user->user_email );
}
if ( is_callable( array( 'WCS_Select2', 'render' ) ) ) {
WCS_Select2::render(
array(
'class' => 'wc-customer-search',
'name' => 'recipient_user',
'id' => 'recipient_user',
'placeholder' => esc_attr__( 'Search for a recipient&hellip;', 'woocommerce-subscriptions' ),
'selected' => $user_string,
'value' => $user_id,
'allow_clear' => 'true',
)
);
} else {
?>
<input type="hidden" class="wc-customer-search" id="recipient_user" name="recipient_user" data-placeholder="<?php esc_attr_e( 'Search for a recipient&hellip;', 'woocommerce-subscriptions' ); ?>" data-selected="<?php echo esc_attr( $user_string ); ?>" value="<?php echo esc_attr( $user_id ); ?>" data-allow_clear="true"/>
<?php
}
?>
</p>
<?php
}
/**
* Save subscription recipient user meta by updating or deleting _recipient_user post meta.
* Also updates the recipient id stored in subscription line item meta.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
*/
public static function save_subscription_recipient_meta( $post_id, $post ) {
if ( 'shop_subscription' !== WC_Data_Store::load( 'subscription' )->get_order_type( $post_id ) || empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
return;
}
$recipient_user = empty( $_POST['recipient_user'] ) ? '' : absint( $_POST['recipient_user'] );
$subscription = wcs_get_subscription( $post_id );
$customer_user = $subscription->get_user_id();
$is_gifted_subscription = WCS_Gifting::is_gifted_subscription( $subscription );
if ( $recipient_user == $customer_user ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
// Remove the recipient.
$recipient_user = '';
wcs_add_admin_notice( __( 'Error saving subscription recipient: customer and recipient cannot be the same. The recipient user has been removed.', 'woocommerce-subscriptions' ), 'error' );
}
if ( ( $is_gifted_subscription && WCS_Gifting::get_recipient_user( $subscription ) == $recipient_user ) || ( ! $is_gifted_subscription && empty( $recipient_user ) ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
// Recipient user remains unchanged - do nothing.
return;
} elseif ( empty( $recipient_user ) ) {
WCS_Gifting::delete_recipient_user( $subscription );
// Delete recipient meta from subscription order items.
foreach ( $subscription->get_items() as $order_item_id => $order_item ) {
wc_delete_order_item_meta( $order_item_id, 'wcsg_recipient' );
}
} else {
WCS_Gifting::set_recipient_user( $subscription, $recipient_user );
// Update all subscription order items.
foreach ( $subscription->get_items() as $order_item_id => $order_item ) {
wc_update_order_item_meta( $order_item_id, 'wcsg_recipient', 'wcsg_recipient_id_' . $recipient_user );
}
}
}
/**
* Outputs a welcome message. Called when the Subscriptions extension is activated.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function admin_installed_notice() {
if ( true == get_transient( 'wcsg_show_activation_notice' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
wc_get_template( 'activation-notice.php', array( 'settings_tab_url' => self::settings_tab_url() ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
delete_transient( 'wcsg_show_activation_notice' );
}
}
/**
* A WooCommerce version aware function for getting the Subscriptions/Gifting admin settings tab URL.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
* @return string
*/
public static function settings_tab_url() {
return apply_filters( 'woocommerce_subscriptions_settings_tab_url', admin_url( 'admin.php?page=wc-settings&tab=subscriptions' ) );
}
/**
* Adds a dropdown to the Subscriptions admin screen to allow filtering by gifted subscriptions.
*
* @param string $post_type Current post type.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.3.
*/
public static function add_gifted_subscriptions_filter( $post_type = '' ) {
$post_type = ! empty( $post_type ) ? $post_type : $GLOBALS['typenow'];
if ( 'shop_subscription' !== $post_type ) {
return;
}
$gifted_subscriptions = WCS_Gifting::get_gifted_subscriptions(
array(
'limit' => 1,
'return' => 'ids',
)
);
if ( empty( $gifted_subscriptions ) ) {
return;
}
$options = array(
'' => __( 'All Subscriptions', 'woocommerce-subscriptions' ),
'true' => __( 'Gifted Subscriptions', 'woocommerce-subscriptions' ),
'false' => __( 'Non-gifted subscriptions', 'woocommerce-subscriptions' ),
);
$wcsg_is_gifted = isset( $_GET['wcsg_is_gifted'] ) ? $_GET['wcsg_is_gifted'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
echo '<select class="wcsg_is_gifted_selector last" name="wcsg_is_gifted" id="wcsg_is_gifted">';
foreach ( $options as $value => $text ) {
echo '<option value="' . esc_attr( $value ) . '" ' . selected( $value, $wcsg_is_gifted, false ) . '>' . esc_html( $text ) . '</option>';
}
echo '</select>';
}
/**
* Filters the main admin query to include only gifted or non-gifted subscriptions.
*
* @param WP_Query $query The WP_Query instance (passed by reference).
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.3.
*/
public static function maybe_filter_by_gifted_subscriptions( $query ) {
global $typenow;
if ( ! is_admin() || ! $query->is_main_query() || 'shop_subscription' !== $typenow || ! isset( $_GET['wcsg_is_gifted'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
$wcsg_is_gifted = trim( $_GET['wcsg_is_gifted'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$compare = '';
if ( 'true' === $wcsg_is_gifted ) {
$compare = 'EXISTS';
} elseif ( 'false' === $wcsg_is_gifted ) {
$compare = 'NOT EXISTS';
}
if ( ! $compare ) {
return;
}
$meta_query = $query->get( 'meta_query' );
if ( ! $meta_query ) {
$meta_query = array();
}
$meta_query[] = array(
'key' => '_recipient_user',
'compare' => $compare,
);
$query->set( 'meta_query', $meta_query );
}
/**
* Filter the Subscriptions Admin Table to filter only gifted or non-gifted subscriptions.
*
* @param array $request_query The query args sent to wc_get_orders().
*/
public static function filter_subscription_list_table_by_gifted_subscriptions( $request_query ) {
/**
* Note this request isn't nonced as we're only filtering a list table and not modifying data.
*/
if ( ! isset( $_GET['wcsg_is_gifted'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
return $request_query;
}
$wcsg_is_gifted = trim( $_GET['wcsg_is_gifted'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( ! in_array( $wcsg_is_gifted, array( 'true', 'false' ), true ) ) {
return $request_query;
}
$request_query['meta_query'][] = array(
'key' => '_recipient_user',
'compare' => 'true' === $wcsg_is_gifted ? 'EXISTS' : 'NOT EXISTS',
);
return $request_query;
}
/**
* Adds actions to the admin edit subscriptions page, if the subscription is a gifted one.
*
* @param array $actions Current admin actions.
* @return array $actions The subscription actions with the "Renew Now" action added if it's permitted.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
public static function add_resend_new_recipient_account_email_action( $actions ) {
global $theorder;
if ( WCS_Gifting::is_gifted_subscription( $theorder ) ) {
$actions['wcsg_resend_new_recipient_account_email'] = __( 'Resend "new recipient account" email', 'woocommerce-subscriptions' );
}
return $actions;
}
/**
* Resends the "new recipient" e-mail.
*
* @param WC_Order $subscription Subscription object.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
public static function resend_new_recipient_account_email( $subscription ) {
if ( WCS_Gifting::is_gifted_subscription( $subscription->get_id() ) ) {
WCSG_Email::resend_new_recipient_user_email( $subscription );
}
}
/**
* Get enable gifting setting.
*
* @return bool
*/
public static function is_gifting_enabled() {
$global_enable_gifting = get_option( self::$option_prefix . '_enable_gifting', 'no' );
return apply_filters( 'wcsg_enable_gifting', 'yes' === $global_enable_gifting );
}
/**
* Get if gifting is enabled by default for all products.
*
* @return bool
*/
public static function is_gifting_enabled_for_all_products() {
$global_default_option = get_option( self::$option_prefix . '_default_option', 'disabled' );
return apply_filters( 'wcsg_is_enabled_for_all_products', 'enabled' === $global_default_option );
}
/**
* Get the text for the gifting option.
*
* @return string
*/
public static function get_gifting_option_text() {
return self::is_gifting_enabled_for_all_products()
? __( 'Follow global setting (enabled)', 'woocommerce-subscriptions' )
: __( 'Follow global setting (disabled)', 'woocommerce-subscriptions' );
}
/**
* Get the text for the gifting option.
*/
public static function get_gifting_global_override_text() {
?>
<p class="_subscription_gifting_field_description form-field">
<span class="description">
<?php
/* translators: %1$s opening anchor tag with url, %2$s closing anchor tag */
$gifting_description = __( 'Overriding your %1$sstore\'s settings%2$s', 'woocommerce-subscriptions' );
echo wp_kses_post(
sprintf(
$gifting_description,
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=subscriptions#woocommerce_subscriptions_gifting_enable_gifting' ) . '">',
'</a>'
)
);
?>
</span>
</p>
<?php
}
}

View File

@@ -0,0 +1,418 @@
<?php
/**
* Cart integration.
*
* @package WooCommerce Subscriptions Gifting
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class for cart integration.
*/
class WCSG_Cart {
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'woocommerce_after_cart_item_name', __CLASS__ . '::print_gifting_option_cart', 10, 2 );
add_filter( 'woocommerce_widget_cart_item_quantity', __CLASS__ . '::add_gifting_option_minicart', 1, 3 );
add_filter( 'woocommerce_update_cart_action_cart_updated', __CLASS__ . '::cart_update', 1, 1 );
add_filter( 'woocommerce_add_to_cart_validation', __CLASS__ . '::prevent_products_in_gifted_renewal_orders', 10 );
add_filter( 'woocommerce_order_again_cart_item_data', __CLASS__ . '::add_recipient_to_resubscribe_initial_payment_item', 10, 3 );
add_filter( 'woocommerce_order_again_cart_item_data', __CLASS__ . '::remove_recipient_from_order_again_cart_item_meta', 10, 1 );
add_filter( 'woocommerce_checkout_create_order_line_item', __CLASS__ . '::add_recipient_to_order_line_item', 10, 4 );
if ( did_action( 'woocommerce_blocks_loaded' ) ) {
self::register_blocks_update_callback();
} else {
add_action( 'woocommerce_blocks_loaded', __CLASS__ . '::register_blocks_update_callback' );
}
}
/**
* Adds the wcsg_cart_key meta to the order line item
* So it's possible to track which subscription came from which parent order line item.
*
* @param WC_Order_Item_Product $item The order line item.
* @param string $cart_item_key The cart item key.
* @param array $values The cart item values.
* @param WC_Order $order The order.
*/
public static function add_recipient_to_order_line_item( $item, $cart_item_key, $values, $order ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
if ( ! isset( $values['wcsg_gift_recipients_email'] ) ) {
return;
}
$item->add_meta_data( '_wcsg_cart_key', $cart_item_key );
}
/**
* Registers the blocks cart update callback.
*/
public static function register_blocks_update_callback() {
woocommerce_store_api_register_update_callback(
array(
'namespace' => 'wcsg-cart',
'callback' => __CLASS__ . '::handle_blocks_update_cart_item_recipient',
)
);
}
/**
* Handles the blocks cart update callback.
*
* @param array $data The data from the blocks update callback.
*/
public static function handle_blocks_update_cart_item_recipient( $data ) {
if ( ! isset( $data['recipient'] ) || ! isset( $data['itemKey'] ) ) {
return;
}
$recipient = $data['recipient'];
$key = $data['itemKey'];
if ( ! WCS_Gifting::validate_recipient_emails( array( $recipient ) ) ) {
return;
}
if ( ! isset( WC()->cart->cart_contents[ $key ] ) ) {
return;
}
WC()->cart->cart_contents[ $key ]['wcsg_gift_recipients_email'] = $recipient;
}
/**
* Adds gifting ui elements to subscription cart items.
*
* @param string $title The product title displayed in the cart table.
* @param array $cart_item Details of an item in WC_Cart.
* @param string $cart_item_key The key of the cart item being displayed in the cart table.
*/
public static function add_gifting_option_cart( $title, $cart_item, $cart_item_key ) {
$is_mini_cart = did_action( 'woocommerce_before_mini_cart' ) && ! did_action( 'woocommerce_after_mini_cart' );
if ( is_cart() && ! $is_mini_cart ) {
$title .= self::maybe_display_gifting_information( $cart_item, $cart_item_key );
}
return $title;
}
/**
* Adds gifting ui elements to subscription items in the mini cart.
*
* @param int $quantity The quantity of the cart item.
* @param array $cart_item Details of an item in WC_Cart.
* @param string $cart_item_key Key of the cart item being displayed in the mini cart.
*/
public static function add_gifting_option_minicart( $quantity, $cart_item, $cart_item_key ) {
$recipient_email = '';
$html_string = '';
if ( self::contains_gifted_renewal() ) {
$recipient_user_id = self::get_recipient_from_cart_item( wcs_cart_contains_renewal() );
$recipient_user = get_userdata( $recipient_user_id );
if ( $recipient_user ) {
$recipient_email = $recipient_user->user_email;
}
} elseif ( ! empty( $cart_item['wcsg_gift_recipients_email'] ) ) {
$recipient_email = $cart_item['wcsg_gift_recipients_email'];
}
if ( '' !== $recipient_email ) {
ob_start();
wc_get_template( 'html-flat-gifting-recipient-details.php', array( 'email' => $recipient_email ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
$html_string = ob_get_clean();
}
return $quantity . $html_string;
}
/**
* Updates the cart items for changes made to recipient infomation on the cart page.
*
* @param bool $cart_updated whether the cart has been updated.
*/
public static function cart_update( $cart_updated ) {
if ( ! empty( $_POST['recipient_email'] ) ) {
if ( ! empty( $_POST['_wcsgnonce'] ) && wp_verify_nonce( $_POST['_wcsgnonce'], 'wcsg_add_recipient' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$recipients = $_POST['recipient_email']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
WCS_Gifting::validate_recipient_emails( $recipients );
foreach ( WC()->cart->cart_contents as $key => $item ) {
if ( isset( $_POST['recipient_email'][ $key ] ) ) {
WCS_Gifting::update_cart_item_recipient( $item, $key, $_POST['recipient_email'][ $key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
}
}
} else {
wc_add_notice( __( 'There was an error with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
}
}
return $cart_updated;
}
/**
* Prevent products being added to the cart if the cart contains a gifted subscription renewal.
*
* @param bool $passed Whether adding to cart is valid.
*/
public static function prevent_products_in_gifted_renewal_orders( $passed ) {
if ( $passed ) {
foreach ( WC()->cart->cart_contents as $key => $item ) {
if ( isset( $item['subscription_renewal'] ) ) {
$subscription = wcs_get_subscription( $item['subscription_renewal']['subscription_id'] );
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
$passed = false;
wc_add_notice( __( 'You cannot add additional products to the cart. Please pay for the subscription renewal first.', 'woocommerce-subscriptions' ), 'error' );
break;
}
}
}
}
return $passed;
}
/**
* Determines if a cart item is able to be gifted.
* Only subscriptions that are not a renewal or switch subscription are giftable.
*
* @param array $cart_item Cart item.
* @return bool Whether the cart item is giftable.
*/
public static function is_giftable_item( $cart_item ) {
return WCSG_Product::is_giftable( $cart_item['data'] ) && ! isset( $cart_item['subscription_renewal'] ) && ! isset( $cart_item['subscription_switch'] );
}
/**
* Returns the relevant html (static/flat, interactive or none at all) depending on
* whether the cart item is a giftable cart item or is a gifted renewal item.
*
* @param array $cart_item The cart item.
* @param string $cart_item_key The cart item key.
* @param string $print_or_return Wether to print or return the HTML content. Optional. Default behaviour is to return the string. Pass 'print' to print the HTML content directly.
* @return string Returns the HTML string if $print_or_return is set to 'return', otherwise prints the HTML and nothing is returned.
*/
public static function maybe_display_gifting_information( $cart_item, $cart_item_key, $print_or_return = 'return' ) {
$output = '';
if ( self::is_giftable_item( $cart_item ) ) {
$email = ( empty( $cart_item['wcsg_gift_recipients_email'] ) ) ? '' : $cart_item['wcsg_gift_recipients_email'];
$output = WCS_Gifting::render_add_recipient_fields( $email, $cart_item_key, 'return' );
} elseif ( self::contains_gifted_renewal() ) {
$recipient_user_id = self::get_recipient_from_cart_item( wcs_cart_contains_renewal() );
$recipient_user = get_userdata( $recipient_user_id );
if ( $recipient_user ) {
$output = wc_get_template_html(
'html-flat-gifting-recipient-details.php',
array( 'email' => $recipient_user->user_email ),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
);
}
}
if ( 'return' === $print_or_return ) {
return $output;
} else {
echo wp_kses(
$output,
array(
'fieldset' => array(),
'input' => array(
'type' => array(),
'id' => array(),
'class' => array(),
'style' => array(),
'value' => array(),
'checked' => array(),
'disabled' => array(),
'data-recipient' => array(),
'name' => array(),
'placeholder' => array(),
),
'label' => array(
'for' => array(),
),
'div' => array(
'class' => array(),
'style' => array(),
),
'p' => array(
'class' => array(),
'style' => array(),
'id' => array(),
),
'svg' => array(
'xmlns' => array(),
'viewbox' => array(),
'width' => array(),
'height' => array(),
'aria-hidden' => array(),
'focusable' => array(),
),
'path' => array(
'd' => array(),
),
'span' => array(),
)
);
}
return '';
}
/**
* When setting up the cart for resubscribes or initial subscription payment carts, ensure the existing subscription recipient email is added to the cart item.
*
* @param array $cart_item_data Cart item data.
* @param array $line_item Line item.
* @param object $subscription Subscription object.
* @return array Updated cart item data.
*/
public static function add_recipient_to_resubscribe_initial_payment_item( $cart_item_data, $line_item, $subscription ) {
$recipient_user_id = 0;
if ( $subscription instanceof WC_Order && isset( $line_item['wcsg_recipient'] ) ) {
$recipient_user_id = substr( $line_item['wcsg_recipient'], strlen( 'wcsg_recipient_id_' ) );
} elseif ( ! array_key_exists( 'subscription_renewal', $cart_item_data ) && WCS_Gifting::is_gifted_subscription( $subscription ) ) {
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
}
if ( ! empty( $recipient_user_id ) ) {
$recipient_user = get_userdata( $recipient_user_id );
if ( $recipient_user ) {
$cart_item_data['wcsg_gift_recipients_email'] = $recipient_user->user_email;
}
}
return $cart_item_data;
}
/**
* Checks the cart to see if it contains a gifted subscription renewal.
*
* @return bool
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.
*/
public static function contains_gifted_renewal() {
$cart_contains_gifted_renewal = false;
$item = wcs_cart_contains_renewal();
if ( $item ) {
$cart_contains_gifted_renewal = WCS_Gifting::is_gifted_subscription( $item['subscription_renewal']['subscription_id'] );
}
return $cart_contains_gifted_renewal;
}
/**
* Checks the cart to see if a gift recipient email is set.
*
* @return bool
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.
*/
public static function contains_gift_recipient_email() {
$has_recipient_email = false;
if ( ! empty( WC()->cart->cart_contents ) ) {
foreach ( WC()->cart->cart_contents as $cart_item ) {
if ( isset( $cart_item['wcsg_gift_recipients_email'] ) ) {
$has_recipient_email = true;
break;
}
}
}
return $has_recipient_email;
}
/**
* Retrieve a recipient user's ID from a cart item.
*
* @param array $cart_item Cart item.
* @return string the recipient id. If the cart item doesn't belong to a recipient an empty string is returned
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
*/
public static function get_recipient_from_cart_item( $cart_item ) {
$recipient_email = '';
$recipient_user_id = '';
if ( isset( $cart_item['subscription_renewal'] ) && WCS_Gifting::is_gifted_subscription( $cart_item['subscription_renewal']['subscription_id'] ) ) {
$recipient_id = WCS_Gifting::get_recipient_user( wcs_get_subscription( $cart_item['subscription_renewal']['subscription_id'] ) );
$recipient = get_user_by( 'id', $recipient_id );
$recipient_email = $recipient->user_email;
} elseif ( isset( $cart_item['wcsg_gift_recipients_email'] ) ) {
$recipient_email = $cart_item['wcsg_gift_recipients_email'];
}
if ( ! empty( $recipient_email ) ) {
$recipient_user_id = email_exists( $recipient_email );
}
return $recipient_user_id;
}
/**
* Remove recipient line item meta from order again cart item meta. This meta is re-added to the line item after
* checkout and so doesn't need to copied through the cart in this way.
*
* @param array $cart_item_data Cart item data.
* @return array Updated cart item data.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
*/
public static function remove_recipient_from_order_again_cart_item_meta( $cart_item_data ) {
foreach ( array( 'subscription_renewal', 'subscription_resubscribe', 'subscription_initial_payment' ) as $subscription_order_again_key ) {
if ( isset( $cart_item_data[ $subscription_order_again_key ]['custom_line_item_meta']['wcsg_recipient'] ) ) {
unset( $cart_item_data[ $subscription_order_again_key ]['custom_line_item_meta']['wcsg_recipient'] );
}
}
return $cart_item_data;
}
/**
* Maybe print gifting HTML elements.
*
* @param array $cart_item The cart item array data.
* @param string $cart_item_key The cart item key.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
*/
public static function print_gifting_option_cart( $cart_item, $cart_item_key ) {
self::maybe_display_gifting_information( $cart_item, $cart_item_key, 'print' );
}
/** Deprecated **/
/**
* Returns gifting ui html elements displaying the email of the recipient.
*
* @param string $cart_item_key The key of the cart item being displayed in the mini cart.
* @param string $email The email of the gift recipient.
* @deprecated 2.0.1
*/
public static function generate_static_gifting_html( $cart_item_key, $email ) {
wcs_deprecated_function( __METHOD__, '2.0.1', "the 'html-flat-gifting-recipient-details.php' template. For example usage, see " . __METHOD__ );
ob_start();
wc_get_template( 'html-flat-gifting-recipient-details.php', array( 'email' => $email ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
return ob_get_clean();
}
}

View File

@@ -0,0 +1,208 @@
<?php
/**
* Checkout integration.
*
* @package WooCommerce Subscriptions Gifting
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class for checkout integration.
*/
class WCSG_Checkout {
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'woocommerce_checkout_cart_item_quantity', __CLASS__ . '::add_gifting_option_checkout', 1, 3 );
add_action( 'woocommerce_checkout_subscription_created', __CLASS__ . '::subscription_created', 1, 3 );
add_filter( 'woocommerce_subscriptions_recurring_cart_key', __CLASS__ . '::add_recipient_email_recurring_cart_key', 1, 2 );
add_action( 'woocommerce_checkout_process', __CLASS__ . '::update_cart_before_checkout' );
add_filter( 'woocommerce_ship_to_different_address_checked', __CLASS__ . '::maybe_ship_to_recipient', 100, 1 );
add_filter( 'woocommerce_checkout_get_value', __CLASS__ . '::maybe_get_recipient_shipping', 10, 2 );
add_action( 'woocommerce_checkout_update_order_review', __CLASS__ . '::store_recipients_in_session', 10, 1 );
add_action( 'woocommerce_before_checkout_shipping_form', __CLASS__ . '::maybe_display_recipient_shipping_notice', 10 );
add_filter( 'woocommerce_get_item_data', __CLASS__ . '::woocommerce_get_item_data', 10, 2 );
}
/**
* Adds gifting ui elements to the checkout page. Also updates recipient information
* stored on the cart item from session data if it exists.
*
* @param int $quantity Quantity.
* @param object $cart_item The Cart_Item for which we are adding ui elements.
* @param string $cart_item_key Cart item key.
* @return int The quantity of the cart item with ui elements appended on.
*/
public static function add_gifting_option_checkout( $quantity, $cart_item, $cart_item_key ) {
return $quantity . WCSG_Cart::maybe_display_gifting_information( $cart_item, $cart_item_key );
}
/**
* Attaches the recipient email address to a subscription when it is purchased via checkout.
*
* @param WC_Subscription $subscription The subscription that has just been created.
* @param WC_Order $order Order object.
* @param WC_Cart $recurring_cart An array of subscription products that make up the subscription.
*/
public static function subscription_created( $subscription, $order, $recurring_cart ) {
$cart_item = reset( $recurring_cart->cart_contents );
if ( ! empty( $cart_item['wcsg_gift_recipients_email'] ) ) {
$subscription->update_meta_data( '_recipient_user_email_address', $cart_item['wcsg_gift_recipients_email'] );
$subscription->save();
}
}
/**
* Attaches the recipient email to a recurring cart key to differentiate subscription products
* gifted to different recipients.
*
* @param string $cart_key Cart key.
* @param object $cart_item Cart item.
* @return string The cart_key with a recipient's email appended
*/
public static function add_recipient_email_recurring_cart_key( $cart_key, $cart_item ) {
if ( ! empty( $cart_item['wcsg_gift_recipients_email'] ) ) {
$cart_key .= '_' . $cart_item['wcsg_gift_recipients_email'];
}
return $cart_key;
}
/**
* Updates the cart items for changes made to recipient infomation on the checkout page.
* This needs to occur right before WooCommerce processes the cart.
* If an error occurs schedule a checkout reload so the user can see the emails causing the errors.
*/
public static function update_cart_before_checkout() {
if ( ! empty( $_POST['recipient_email'] ) ) {
if ( ! empty( $_POST['_wcsgnonce'] ) && wp_verify_nonce( $_POST['_wcsgnonce'], 'wcsg_add_recipient' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$recipients = $_POST['recipient_email']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( ! WCS_Gifting::validate_recipient_emails( $recipients ) ) {
WC()->session->set( 'reload_checkout', true );
}
foreach ( WC()->cart->cart_contents as $key => $item ) {
if ( isset( $_POST['recipient_email'][ $key ] ) ) {
WCS_Gifting::update_cart_item_recipient( $item, $key, $_POST['recipient_email'][ $key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
}
}
} else {
wc_add_notice( __( 'There was an error with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
}
}
}
/**
* If the cart contains a gifted subscription renewal or a gift recipient, tell the checkout to ship to a different address.
*
* @param bool $ship_to_different_address Whether the order will ship to a different address.
* @return bool
*/
public static function maybe_ship_to_recipient( $ship_to_different_address ) {
if ( ! $ship_to_different_address && ( WCSG_Cart::contains_gifted_renewal() || WCSG_Cart::contains_gift_recipient_email() ) ) {
$ship_to_different_address = true;
}
return $ship_to_different_address;
}
/**
* Returns recipient's shipping address if the checkout is requesting
* the shipping fields for a gifted subscription renewal.
*
* @param string $value Default checkout field value.
* @param string $key The checkout form field name/key.
*/
public static function maybe_get_recipient_shipping( $value, $key ) {
$shipping_fields = WC()->countries->get_address_fields( '', 'shipping_' );
if ( array_key_exists( $key, $shipping_fields ) && WCSG_Cart::contains_gifted_renewal() ) {
$item = wcs_cart_contains_renewal();
$subscription = wcs_get_subscription( $item['subscription_renewal']['subscription_id'] );
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
$value = get_user_meta( $recipient_user_id, $key, true );
}
return $value;
}
/**
* Stores recipient email data in the session to prevent losing changes made to recipient emails
* during the checkout updating the order review fields.
*
* @param string $checkout_data Checkout _POST data in a query string format.
*/
public static function store_recipients_in_session( $checkout_data ) {
parse_str( $checkout_data, $checkout_data );
if ( isset( $checkout_data['recipient_email'] ) ) {
// Store recipient emails on the cart items so they can be repopulated after checkout update.
foreach ( WC()->cart->cart_contents as $key => $item ) {
if ( isset( $checkout_data['recipient_email'][ $key ] ) ) {
WCS_Gifting::update_cart_item_recipient( $item, $key, $checkout_data['recipient_email'][ $key ] );
}
}
}
}
/**
* Output a notice to guide the shopper on how to fill the shipping address. The visibility of this notice is controlled by CSS depending on
* the status of the gifting checkbox.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.
*/
public static function maybe_display_recipient_shipping_notice() {
wc_print_notice( esc_html__( 'Please enter the gift recipients shipping address here, or well collect it directly from them when they log in.', 'woocommerce-subscriptions' ), 'notice' );
}
/**
* Adds meta data so it can be displayed in the Cart.
*/
public static function woocommerce_get_item_data( $other_data, $cart_item ) {
$product = $cart_item['data'];
if ( ! WC_Subscriptions_Product::is_subscription( $product ) || ! WCSG_Product::is_giftable( $product ) ) {
return $other_data;
}
$other_data[] = array(
'name' => 'item_key',
'value' => $cart_item['key'],
'hidden' => true,
'__experimental_woocommerce_blocks_hidden' => false,
);
$gift_recipient = $cart_item['wcsg_gift_recipients_email'] ?? '';
if ( $gift_recipient ) {
$other_data[] = array(
'name' => __( 'Gifting to', 'woocommerce-subscriptions' ),
'value' => $gift_recipient,
'hidden' => true,
'__experimental_woocommerce_blocks_hidden' => false,
);
$other_data[] = array(
'name' => 'gifting_to_hidden',
'value' => $gift_recipient,
'hidden' => true,
'__experimental_woocommerce_blocks_hidden' => false,
);
}
return $other_data;
}
}

View File

@@ -0,0 +1,623 @@
<?php
/**
* Downloadable files access policy for gifted subscriptions.
*
* @package WooCommerce Subscriptions Gifting
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Properly handles permissions and access for downloadable files associated to gifted subscriptions.
*/
class WCSG_Download_Handler {
/**
* Cache of subscription download permissions.
*
* @var array
*/
private static $subscription_download_permissions = array();
/**
* Temporary cache of recipient download permissions stored before a subscription is saved.
*
* @var array
*/
private static $recipient_download_permissions = array();
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'woocommerce_subscription_settings', __CLASS__ . '::register_download_settings', 11, 1 );
add_filter( 'woocommerce_downloadable_file_permission_data', __CLASS__ . '::grant_recipient_download_permissions', 11 );
add_filter( 'woocommerce_get_item_downloads', __CLASS__ . '::get_item_download_links', 15, 3 );
// Download Permission Meta Box Functions.
add_action( 'woocommerce_process_shop_order_meta', __CLASS__ . '::download_permissions_meta_box_save', 10, 1 );
add_action( 'woocommerce_admin_order_data_after_order_details', __CLASS__ . '::get_download_permissions_before_meta_box', 10, 1 );
add_filter( 'woocommerce_admin_download_permissions_title', __CLASS__ . '::add_user_to_download_permission_title', 10, 3 );
// Grant access via download meta box - hooked on prior to WC_AJAX::grant_access_to_download().
add_action( 'wp_ajax_woocommerce_grant_access_to_download', __CLASS__ . '::ajax_grant_download_permission', 9 );
// Revoke access via download meta box - hooked to a custom Ajax handler in place of WC_AJAX::revoke_access_to_download().
add_action( 'wp_ajax_wcsg_revoke_access_to_download', __CLASS__ . '::ajax_revoke_download_permission' );
// Handle subscriptions created on the admin. Needs to be hooked on prior WCS_Download_Handler::grant_permissions_on_admin_created_subscription().
add_action( 'woocommerce_admin_created_subscription', array( __CLASS__, 'grant_permissions_on_admin_created_subscription' ), 1 );
// Handle recipient download permissions when the subscription's customer or recipient is changed.
add_action( 'woocommerce_before_subscription_object_save', array( __CLASS__, 'maybe_store_recipient_permissions_before_save' ) );
add_action( 'woocommerce_order_object_updated_props', array( __CLASS__, 'maybe_restore_recipient_permissions_after_save' ), 10, 2 );
add_action( 'woocommerce_subscriptions_gifting_recipient_changed', array( __CLASS__, 'maybe_grant_permissions_to_new_recipient' ), 10, 3 );
}
/**
* Gets the correct user's download links for a downloadable order item.
* If the request is from within an email, the links belonging to the email recipient are returned otherwise
* if the request is from the view subscription page use the current user id,
* otherwise the links for order's customer user are returned.
*
* @param array $files Downloadable files for the order item.
* @param array $item Order line item.
* @param object $order Order object.
* @return array $files Files.
*/
public static function get_item_download_links( $files, $item, $order ) {
$recipient_user_id = WCS_Gifting::get_recipient_user( $order );
if ( $recipient_user_id ) {
$user_id = ( wcs_is_subscription( $order ) && wcs_is_view_subscription_page() ) ? get_current_user_id() : $order->get_user_id();
$mailer = WC()->mailer();
foreach ( $mailer->emails as $email ) {
if ( isset( $email->wcsg_sending_recipient_email ) ) {
$user_id = $recipient_user_id;
break;
}
}
$files = self::get_user_downloads_for_order_item( $order, $user_id, $item );
}
return $files;
}
/**
* Grants download permissions to the recipient rather than the purchaser by default. However if the
* purchaser can download setting is selected, permissions are granted to both recipient and purchaser.
*
* @param array $data download permission data inserted into the wp_woocommerce_downloadable_product_permissions table.
* @return array $data
*/
public static function grant_recipient_download_permissions( $data ) {
$subscription = wcs_get_subscription( $data['order_id'] );
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
$can_purchaser_download = ( 'yes' === get_option( WCSG_Admin::$option_prefix . '_downloadable_products', 'no' ) ) ? true : false;
if ( $can_purchaser_download ) {
remove_filter( 'woocommerce_downloadable_file_permission_data', __CLASS__ . '::grant_recipient_download_permissions', 11 );
wc_downloadable_file_permission( $data['download_id'], $data['product_id'], $subscription );
add_filter( 'woocommerce_downloadable_file_permission_data', __CLASS__ . '::grant_recipient_download_permissions', 11 );
}
$recipient_id = WCS_Gifting::get_recipient_user( $subscription );
$recipient = get_user_by( 'id', $recipient_id );
$data['user_id'] = $recipient_id;
$data['user_email'] = $recipient->user_email;
}
return $data;
}
/**
* Insert Gifting download specific settings into Subscriptions settings
*
* @param array $settings Subscription's current set of settings.
* @return array $settings new settings with appended wcsg specific settings.
*/
public static function register_download_settings( $settings ) {
$insert_index = array_search( // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
array(
'type' => 'sectionend',
'id' => WCSG_Admin::$option_prefix,
),
$settings
);
array_splice(
$settings,
$insert_index,
0,
array(
array(
'name' => __( 'Downloadable Products', 'woocommerce-subscriptions' ),
'desc' => __( 'Allow both purchaser and recipient to download subscription products.', 'woocommerce-subscriptions' ),
'id' => WCSG_Admin::$option_prefix . '_downloadable_products',
'default' => 'no',
'type' => 'checkbox',
'desc_tip' => __( 'If you want both the recipient and purchaser of a subscription to have access to downloadable products.', 'woocommerce-subscriptions' ),
),
)
);
return $settings;
}
/**
* Before displaying the meta box, save an unmodified set of the download permissions so they can be used later
* when displaying user information and outputting download permission hidden fields (which needs to be done just
* once per permission).
*
* @param WC_Subscription $subscription Subscription object.
*/
public static function get_download_permissions_before_meta_box( $subscription ) {
global $wpdb;
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
self::$subscription_download_permissions = self::get_subscription_download_permissions( wcsg_get_objects_id( $subscription ) );
}
}
/**
* Formats the download permission title to also include information about the user the permission belongs to.
* This is to make it clear to store managers which user's permissions are being edited.
*
* We also sneak in hidden fields for the user and permission ID to make sure that we can revoke or modify
* permissions for a specific user, because WC doesn't use permission IDs and instead uses download IDs, which
* are a hash that do not take into account user ID and duplicate permissions for the same product on the same
* order for different users.
*
* @param string $download_title The download permission title displayed in order download permission meta boxes.
* @param int $product_id Product ID.
* @param int $order_id Order ID.
*/
public static function add_user_to_download_permission_title( $download_title, $product_id, $order_id ) {
$subscription = wcs_get_subscription( $order_id );
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
foreach ( self::$subscription_download_permissions as $index => $download ) {
if ( ! isset( $download->displayed ) ) {
?>
<input type="hidden" class="wcsg_download_permission_id" name="wcsg_download_permission_ids[<?php echo esc_attr( $index ); ?>]" value="<?php echo absint( $download->permission_id ); ?>" />
<input type="hidden" class="wcsg_download_permission_id" name="wcsg_download_user_ids[<?php echo esc_attr( $index ); ?>]" value="<?php echo absint( $download->user_id ); ?>" />
<?php
$user_role = ( WCS_Gifting::get_recipient_user( $subscription ) == $download->user_id ) ? __( 'Recipient', 'woocommerce-subscriptions' ) : __( 'Purchaser', 'woocommerce-subscriptions' ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$user = get_userdata( $download->user_id );
$user_name = ucfirst( $user->first_name ) . ( ( ! empty( $user->last_name ) ) ? ' ' . ucfirst( $user->last_name ) : '' );
$download_title = $user_role . ' (' . ( empty( $user_name ) ? ucfirst( $user->display_name ) : $user_name ) . ') &mdash; ' . $download_title;
$download->displayed = true;
break;
}
}
}
return $download_title;
}
/**
* Save download permission meta box data.
*
* We need to unhook WC_Meta_Box_Order_Downloads::save() to prevent the WC save function from being called because
* it does not differentiate between duplicate permissions for the same product on the same order even when the
* permissions are for different users (and with different permission IDs). This means it would modify all
* permissions on that order for that product and set them all to be for the same user, instead of keeping
* them for the different users.
*
* @param int $subscription_id Subscription ID.
*/
public static function download_permissions_meta_box_save( $subscription_id ) {
global $wpdb;
// Post WC 3.0 WC_Meta_Box_Order_Downloads::save() no longer overrides the user ID associated with the download permissions so the contents of this function aren't necessary.
if ( ! wcsg_is_woocommerce_pre( '3.0' ) ) {
return;
}
if ( isset( $_POST['wcsg_download_permission_ids'] ) && isset( $_POST['woocommerce_meta_nonce'] ) && wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
remove_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30 );
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$permission_ids = $_POST['wcsg_download_permission_ids'];
$user_ids = $_POST['wcsg_download_user_ids'];
$download_ids = $_POST['download_id'];
$product_ids = $_POST['product_id'];
$downloads_remaining = $_POST['downloads_remaining'];
$access_expires = $_POST['access_expires'];
// phpcs:enable
$subscription = wcs_get_subscription( $subscription_id );
foreach ( $download_ids as $index => $download_id ) {
$expiry = ( array_key_exists( $index, $access_expires ) && '' != $access_expires[ $index ] ) ? date_i18n( 'Y-m-d', strtotime( $access_expires[ $index ] ) ) : null; // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$data = array(
'downloads_remaining' => wc_clean( $downloads_remaining[ $index ] ),
'access_expires' => $expiry,
);
$format = array( '%s', '%s' );
// if we're updating the purchaser's permissions, update the download user id and email, in case it has changed.
if ( WCS_Gifting::get_recipient_user( $subscription ) != $user_ids[ $index ] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$data['user_id'] = absint( $_POST['customer_user'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
$format[] = '%d';
$data['user_email'] = wc_clean( wp_unslash( $_POST['_billing_email'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
$format[] = '%s';
}
$wpdb->update(
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
$data,
array(
'order_id' => $subscription_id,
'product_id' => absint( $product_ids[ $index ] ),
'download_id' => wc_clean( $download_ids[ $index ] ),
'permission_id' => $permission_ids[ $index ],
),
$format,
array( '%d', '%d', '%s', '%d' )
);
}
}
}
/**
* Get all download permissions for a subscription
*
* @param int $subscription_id Subscription ID.
* @param string $order_by Column to use inside the ORDER BY clause.
*/
private static function get_subscription_download_permissions( $subscription_id, $order_by = 'product_id' ) {
global $wpdb;
// Only allow ordering by permissions_id and product_id (because we can't sanitise $order_by with $wpdb->prepare(), we need it as a column not a string).
if ( 'permission_id' !== $order_by ) {
$order_by = 'product_id';
}
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE order_id = %d ORDER BY %s",
$subscription_id,
$order_by
)
);
}
/**
* Grants download permissions from the edit subscription meta box grant access button.
* Outputs meta box table rows for each permission granted.
*/
public static function ajax_grant_download_permission() {
check_ajax_referer( 'grant-access', 'security' );
if ( ! current_user_can( 'edit_shop_orders' ) ) {
die( -1 );
}
global $wpdb;
$wpdb->hide_errors();
$order_id = isset( $_POST['order_id'] ) ? intval( $_POST['order_id'] ) : 0;
$product_ids = isset( $_POST['product_ids'] ) ? wp_unslash( $_POST['product_ids'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$loop = isset( $_POST['loop'] ) ? intval( $_POST['loop'] ) : 0;
$file_counter = 0;
if ( WCS_Gifting::is_gifted_subscription( $order_id ) ) {
/** @var WC_Subscription $subscription */
$subscription = wcs_get_subscription( $order_id );
$download_permissions = self::get_subscription_download_permissions( $order_id, 'permission_id' );
$file_names = array();
$billing_email = is_callable( array( $subscription, 'get_billing_email' ) ) ? $subscription->get_billing_email() : $subscription->billing_email;
if ( ! $billing_email ) {
die();
}
if ( ! is_array( $product_ids ) ) {
$product_ids = array( $product_ids );
}
foreach ( $product_ids as $product_id ) {
/** @var WC_Product $product */
$product = wc_get_product( $product_id );
$files = is_callable( array( $product, 'get_downloads' ) ) ? $product->get_downloads() : $product->get_files();
if ( $files ) {
foreach ( $files as $download_id => $file ) {
$file_counter ++;
if ( isset( $file['name'] ) ) {
$file_names[ $download_id ] = $file['name'];
} else {
// Translators: placeholder is the number of files.
$file_names[ $download_id ] = sprintf( __( 'File %d', 'woocommerce-subscriptions' ), $file_counter );
}
wc_downloadable_file_permission( $download_id, $product_id, $subscription );
}
}
}
if ( 0 < count( $file_names ) ) {
$updated_download_permissions = self::get_subscription_download_permissions( $order_id, 'permission_id' );
$new_download_permissions = array_diff( array_keys( $updated_download_permissions ), array_keys( $download_permissions ) );
foreach ( $new_download_permissions as $new_download_permission_index ) {
$loop ++;
$download = $updated_download_permissions[ $new_download_permission_index ];
$file_count = $file_names[ $download->download_id ];
self::$subscription_download_permissions[ $loop ] = $download;
if ( class_exists( 'WC_Customer_Download' ) ) {
// Post WC 3.0 the template expects a WC_Customer_Download object rather than stdClass objects.
$download = new WC_Customer_Download( $download );
}
include plugin_dir_path( WC_PLUGIN_FILE ) . 'includes/admin/meta-boxes/views/html-order-download-permission.php';
}
}
die();
}
}
/**
* WooCommerce revokes download permissions based only on the product an order ID, that means when
* revoking downloads on a gift subscription with permissions for both the purchaser and recipient,
* it will revoke both sets of permissions instead of only the permission against which the store
* manager clicked the "Revoke Access" button.
*
* To workaround this, we add the permission ID as a hidden fields against each download permission
* with @see self::add_user_to_download_permission_title(). We then trigger a custom Ajax request
* that passes the permission ID to the server to make sure we only revoke only that permission.
*
* We also need to remove WC's handler, which is the WC_Ajax:;revoke_access_to_download() method attached
* to the 'woocommerce_revoke_access_to_download' Ajax action. To do this, we have out wcsg-admin.js file
* enqueued after WooCommerce's 'wc-admin-order-meta-boxes' script and then in our JavaScript call
* $( '.order_download_permissions' ).off() to remove WooCommerce's Ajax method.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.
*/
public static function ajax_revoke_download_permission() {
global $wpdb;
check_admin_referer( 'revoke_download_permission', 'nonce' );
if ( ! current_user_can( 'edit_shop_orders' ) ) {
die( -1 );
}
$subscription_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
if ( WCS_Gifting::is_gifted_subscription( $subscription_id ) ) {
$permission_id = isset( $_POST['download_permission_id'] ) ? intval( $_POST['download_permission_id'] ) : 0;
if ( ! empty( $permission_id ) ) {
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE order_id = %d AND permission_id = %d", $subscription_id, $permission_id ) );
}
}
die();
}
/**
* Retrieves a user's download permissions for an order.
*
* @param WC_Order $order Order object.
* @param int $user_id User ID.
* @param array $item Order item.
*
* @return array
*/
public static function get_user_downloads_for_order_item( $order, $user_id, $item ) {
global $wpdb;
$product_id = wcs_get_canonical_product_id( $item );
$downloads = $wpdb->get_results(
$wpdb->prepare(
"SELECT *
FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE user_id = %d
AND order_id = %d
AND product_id = %d",
$user_id,
wcsg_get_objects_id( $order ),
$product_id
)
);
$files = array();
/** @var WC_Product $product */
$product = wc_get_product( $product_id );
foreach ( $downloads as $download ) {
if ( $product->has_file( $download->download_id ) ) {
if ( wcsg_is_woocommerce_pre( '3.0' ) ) {
$files[ $download->download_id ] = $product->get_file( $download->download_id );
} else {
$customer_download = new WC_Customer_Download( $download );
/** @var WC_Product_Download $file */
$file = $product->get_file( $download->download_id );
$files[ $download->download_id ] = $file->get_data();
$files[ $download->download_id ]['downloads_remaining'] = $customer_download->get_downloads_remaining();
$files[ $download->download_id ]['access_expires'] = $customer_download->get_access_expires();
}
$files[ $download->download_id ]['download_url'] = add_query_arg(
array(
'download_file' => $product_id,
'order' => $download->order_key,
'email' => $download->user_email,
'key' => $download->download_id,
),
home_url( '/' )
);
}
}
return $files;
}
/**
* Retrieves all the user's download permissions for an order by checking
* for downloads stored on the subscriptions in the order.
*
* @param WC_Order $order Order object.
* @param int $user_id User ID.
*
* @return array
*/
public static function get_user_downloads_for_order( $order, $user_id ) {
$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => array( 'any' ) ) );
$order_downloads = array();
foreach ( $subscriptions as $subscription ) {
foreach ( $subscription->get_items() as $subscription_item ) {
$order_downloads = array_merge( $order_downloads, self::get_user_downloads_for_order_item( $subscription, $user_id, $subscription_item ) );
}
}
return $order_downloads;
}
/**
* Makes sure download permissions on newly created subscriptions (admin-side) are granted after the recipient has
* been set.
*
* @param WC_Subscription $subscription Subscription object.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
*/
public static function grant_permissions_on_admin_created_subscription( $subscription ) {
// Prevent WC Subscriptions from granting permissions to the subscription before Gifting has had a chance
// to save the recipient information.
remove_action( current_action(), array( 'WCS_Download_Handler', 'grant_download_permissions' ) );
// Grant permissions after recipient information has been saved (which happens with priority 50).
add_action( 'woocommerce_process_shop_order_meta', 'wc_downloadable_product_permissions', 60 );
}
/**
* Stores recipient download permissions before a subscription is saved, just in case they are needed later.
*
* @param WC_Subscription $subscription Subscription object.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
*/
public static function maybe_store_recipient_permissions_before_save( $subscription ) {
global $wpdb;
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
if ( ! $recipient_user_id ) {
return;
}
if ( isset( self::$recipient_download_permissions[ $subscription->get_id() ] ) ) {
return;
}
$data_store = WC_Data_Store::load( 'customer-download' );
self::$recipient_download_permissions[ $subscription->get_id() ] = $data_store->get_downloads(
array(
'order_id' => $subscription->get_id(),
'user_id' => $recipient_user_id,
'return' => 'ids',
)
);
}
/**
* Restores recipient download permissions from cached values.
*
* @param WC_Subscription $subscription Subscription object.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
*/
private static function restore_recipient_permissions( $subscription ) {
$subscription = wcs_get_subscription( $subscription );
if ( ! $subscription || empty( self::$recipient_download_permissions[ $subscription->get_id() ] ) ) {
return;
}
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
$recipient = $recipient_user_id ? get_user_by( 'id', $recipient_user_id ) : false;
if ( ! $recipient ) {
return;
}
foreach ( self::$recipient_download_permissions[ $subscription->get_id() ] as $permission_id ) {
$download = new WC_Customer_Download( $permission_id );
$download->set_user_id( $recipient_user_id );
$download->set_user_email( $recipient->user_email );
$download->save();
}
}
/**
* Restores previous recipient permissions when the subscription's customer changes.
* This is required because WC resets all download permissions to the new customer (effectively disabling recipient access) when such a change is made.
*
* @param WC_Order $order Order object.
* @param array $updated_props Properties that changed.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
*/
public static function maybe_restore_recipient_permissions_after_save( $order, $updated_props ) {
global $wpdb;
if ( ! wcs_is_subscription( $order ) || ! array_intersect( array( 'customer_id', 'billing_email' ), $updated_props ) ) {
return;
}
self::restore_recipient_permissions( $order );
}
/**
* Grants new recipients the downloads permissions the previous recipient had.
*
* @param WC_Subscription $subscription Subscription object.
* @param int $new_recipient_id New recipient user ID.
* @param int $old_recipient_id Old recipient user ID.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.2.
*/
public static function maybe_grant_permissions_to_new_recipient( $subscription, $new_recipient_id, $old_recipient_id ) {
global $wpdb;
if ( ! $old_recipient_id || ! $new_recipient_id ) {
return;
}
self::restore_recipient_permissions( $subscription );
}
}

View File

@@ -0,0 +1,464 @@
<?php
/**
* Main class for e-mails.
*
* @package WooCommerce Subscriptions Gifting/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles e-mailing inside Gifting.
*/
class WCSG_Email {
/**
* Header/subject and triggers associated to e-mails with downloadable headings/subjects.
*
* @var array
*/
public static $downloadable_email_data = array(
'customer_completed_order' => array(
'trigger_action' => 'woocommerce_order_status_completed_notification',
'heading_filter' => 'woocommerce_email_heading_customer_completed_order',
'subject_hook' => 'woocommerce_email_subject_customer_completed_order',
),
'customer_completed_renewal_order' => array(
'trigger_action' => 'woocommerce_order_status_completed_renewal_notification',
'heading_filter' => '', // shares woocommerce_email_heading_customer_completed_order.
'subject_hook' => 'woocommerce_subscriptions_email_subject_customer_completed_renewal_order',
),
'customer_completed_switch_order' => array(
'trigger_action' => 'woocommerce_order_status_completed_switch_notification',
'heading_filter' => 'woocommerce_email_heading_customer_switch_order',
'subject_hook' => 'woocommerce_subscriptions_email_subject_customer_completed_switch_order',
),
'recipient_completed_renewal_order' => array(
'trigger_action' => 'woocommerce_order_status_completed_renewal_notification_recipient',
'heading_filter' => '', // shares woocommerce_email_heading_customer_completed_order.
'subject_hook' => '', // shares woocommerce_subscriptions_email_subject_customer_completed_renewal_order.
),
);
/**
* Flag used to indicate that an e-mail with downloadable headings/subjects is being sent.
*
* @var mixed
*/
public static $sending_downloadable_email;
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'woocommerce_email_classes', __CLASS__ . '::add_new_recipient_customer_email', 11, 1 );
add_action( 'woocommerce_init', __CLASS__ . '::hook_email' );
add_action( 'wcs_gifting_email_order_details', array( __CLASS__, 'order_details' ), 10, 4 );
add_action( 'woocommerce_subscriptions_gifting_recipient_email_details', array( __CLASS__, 'get_related_subscriptions_table' ), 10, 3 );
add_action( 'woocommerce_subscriptions_gifting_recipient_email_details', array( __CLASS__, 'get_address_table' ), 11, 3 );
}
/**
* Add WCS Gifting email classes.
*
* @param WC_Email[] $email_classes E-mail classes.
*/
public static function add_new_recipient_customer_email( $email_classes ) {
require_once 'emails/class-wcsg-email-customer-new-account.php';
require_once 'emails/class-wcsg-email-completed-renewal-order.php';
require_once 'emails/class-wcsg-email-processing-renewal-order.php';
require_once 'emails/class-wcsg-email-recipient-new-initial-order.php';
$email_classes['WCSG_Email_Customer_New_Account'] = new WCSG_Email_Customer_New_Account();
$email_classes['WCSG_Email_Completed_Renewal_Order'] = new WCSG_Email_Completed_Renewal_Order();
$email_classes['WCSG_Email_Processing_Renewal_Order'] = new WCSG_Email_Processing_Renewal_Order();
$email_classes['WCSG_Email_Recipient_New_Initial_Order'] = new WCSG_Email_Recipient_New_Initial_Order();
return $email_classes;
}
/**
* Hooks up all of WCS Gifting emails after the WooCommerce object is constructed.
*/
public static function hook_email() {
add_action( 'woocommerce_created_customer', __CLASS__ . '::maybe_remove_wc_new_customer_email', 9, 2 );
add_action( 'woocommerce_created_customer', __CLASS__ . '::send_new_recipient_user_email', 10, 3 );
add_action( 'woocommerce_created_customer', __CLASS__ . '::maybe_reattach_wc_new_customer_email', 11, 2 );
add_action( 'subscriptions_activated_for_order', __CLASS__ . '::maybe_send_recipient_order_emails', 11, 1 );
$renewal_notification_actions = array(
'woocommerce_order_status_pending_to_processing_renewal_notification',
'woocommerce_order_status_pending_to_on-hold_renewal_notification',
'woocommerce_order_status_completed_renewal_notification',
);
foreach ( $renewal_notification_actions as $action ) {
add_action( $action, __CLASS__ . '::maybe_send_recipient_renewal_notification', 12, 1 );
}
// WC 3.1 removed the email subjects and headings which reference downloadable files. Post 3.1 we don't need to worry about reformatting them.
if ( wcsg_is_woocommerce_pre( '3.1' ) ) {
foreach ( self::$downloadable_email_data as $email_id => $hook_data ) {
// Hook on just before default to store a flag of the email being sent.
add_action( $hook_data['trigger_action'], __CLASS__ . '::set_sending_downloadable_email_flag', 9 );
add_action( $hook_data['trigger_action'], __CLASS__ . '::remove_sending_downloadable_email_flag', 11 );
// Hook the subject and heading hooks.
if ( ! empty( $hook_data['heading_filter'] ) ) {
add_filter( $hook_data['heading_filter'], __CLASS__ . '::maybe_change_download_email_heading', 10, 2 );
}
if ( ! empty( $hook_data['subject_hook'] ) ) {
add_filter( $hook_data['subject_hook'], __CLASS__ . '::maybe_change_download_email_heading', 10, 2 );
}
}
// Hook onto emails sent via order actions.
add_action( 'woocommerce_before_resend_order_emails', __CLASS__ . '::set_sending_downloadable_email_flag', 9 );
add_action( 'woocommerce_after_resend_order_email', __CLASS__ . '::remove_sending_downloadable_email_flag', 11 );
}
}
/**
* If an order contains subscriptions with recipient data send an email to the recipient
* notifying them on their new subscription(s)
*
* @param WC_Order|int $order Order ID or instance.
*/
public static function maybe_send_recipient_order_emails( $order ) {
$order_id = $order instanceof WC_Order ? $order->get_id() : $order;
$subscriptions = wcs_get_subscriptions( array( 'order_id' => $order_id ) );
$processed_recipients = array();
if ( empty( $subscriptions ) ) {
return;
}
WC()->mailer();
foreach ( $subscriptions as $subscription ) {
if ( ! WCS_Gifting::is_gifted_subscription( $subscription ) ) {
continue;
}
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
if ( in_array( $recipient_user_id, $processed_recipients ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
continue;
}
$recipient_subscriptions = WCSG_Recipient_Management::get_recipient_subscriptions( $recipient_user_id, $order_id );
do_action( 'wcsg_new_order_recipient_notification', $recipient_user_id, $recipient_subscriptions );
$processed_recipients[] = $recipient_user_id;
}
}
/**
* If a cart item contains recipient data matching the new customer, dont send the core WooCommerce new customer email.
*
* @param int $customer_id The ID of the new customer being created.
* @param array $new_customer_data Array of data associated to the new customer.
*/
public static function maybe_remove_wc_new_customer_email( $customer_id, $new_customer_data ) {
foreach ( WC()->cart->cart_contents as $key => $item ) {
if ( ! empty( $item['wcsg_gift_recipients_email'] ) ) {
if ( $item['wcsg_gift_recipients_email'] == $new_customer_data['user_email'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
remove_action( current_filter(), array( 'WC_Emails', 'send_transactional_email' ) );
break;
}
}
}
}
/**
* If a cart item contains recipient data matching the new customer, reattach the core WooCommerce new customer email.
*
* @param int $customer_id The ID of the new customer being created.
* @param array $new_customer_data Array of data associated to the new customer.
*/
public static function maybe_reattach_wc_new_customer_email( $customer_id, $new_customer_data ) {
foreach ( WC()->cart->cart_contents as $key => $item ) {
if ( ! empty( $item['wcsg_gift_recipients_email'] ) ) {
if ( $item['wcsg_gift_recipients_email'] == $new_customer_data['user_email'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
add_action( current_filter(), array( 'WC_Emails', 'send_transactional_email' ) );
break;
}
}
}
}
/**
* Generates purchaser new recipient user email.
*
* @param int $purchaser_user_id Subscription purchaser user id.
* @param int $recipient_user_id Subscription recipient user id.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
public static function generate_new_recipient_user_email( $purchaser_user_id, $recipient_user_id ) {
$key = get_password_reset_key( get_userdata( $recipient_user_id ) );
if ( ! is_wp_error( $key ) ) {
$subscription_purchaser_name = WCS_Gifting::get_user_display_name( $purchaser_user_id );
WC()->mailer();
do_action( 'wcsg_created_customer_notification', $recipient_user_id, $key, $subscription_purchaser_name );
}
}
/**
* If a cart item contains recipient data matching the new customer, init the mailer and call the notification for new recipient customers.
*
* @param int $customer_id The ID of the new customer being created.
* @param array $new_customer_data Recipient's user data.
* @param bool $password_generated Whether the password has been generated for the customer.
*/
public static function send_new_recipient_user_email( $customer_id, $new_customer_data, $password_generated ) {
foreach ( WC()->cart->cart_contents as $key => $item ) {
if ( isset( $item['wcsg_gift_recipients_email'] ) ) {
if ( $item['wcsg_gift_recipients_email'] === $new_customer_data['user_email'] ) {
self::generate_new_recipient_user_email( get_current_user_id(), $customer_id );
break;
}
}
}
}
/**
* This will get the necessary data to resend the new recipient new email.
*
* @param WC_Order $subscription The subscription we're using to get the purchaser and recipient data.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
public static function resend_new_recipient_user_email( $subscription ) {
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
if ( ! empty( $recipient_user_id ) ) {
self::generate_new_recipient_user_email( $subscription->get_customer_id(), $recipient_user_id );
}
}
/**
* If the order contains a subscription that is being gifted, init the mailer and call the notification for recipient renewal notices.
*
* @param int $order_id The ID of the renewal order with a new status of processing/completed.
*/
public static function maybe_send_recipient_renewal_notification( $order_id ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
if ( ! empty( $subscriptions ) && is_array( $subscriptions ) ) {
$subscription = reset( $subscriptions );
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
WC()->mailer();
do_action( current_filter() . '_recipient', $order_id );
}
}
}
/**
* Formats an email's heading and subject so that the correct one is displayed.
* If for instance the email recipient doesn't have downloads for this order fallback
* to the normal heading and subject,
*
* @param string $heading The email heading or subject.
* @param object $order Order object.
* @return string
*/
public static function maybe_change_download_email_heading( $heading, $order ) {
if ( empty( self::$sending_downloadable_email ) ) {
return $heading;
}
$user_id = $order->get_user_id();
$mailer = WC()->mailer();
$sending_email = null;
foreach ( $mailer->emails as $email ) {
if ( self::$sending_downloadable_email == $email->id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$sending_email = $email;
if ( isset( $email->wcsg_sending_recipient_email ) ) {
$user_id = $email->wcsg_sending_recipient_email;
}
break;
}
}
$order_downloads = WCSG_Download_Handler::get_user_downloads_for_order( $order, $user_id );
$string_to_format = strpos( current_filter(), 'email_heading' ) ? 'heading' : 'subject';
if ( isset( $sending_email ) && empty( $order_downloads ) && isset( $sending_email->{$string_to_format} ) ) {
$heading = $sending_email->format_string( $sending_email->{$string_to_format} );
}
return $heading;
}
/**
* Set a flag to indicate that an email with downloadable headings and subjects is being sent.
* hooked just before the email's trigger function.
*/
public static function set_sending_downloadable_email_flag() {
$current_filter = current_filter();
if ( 'woocommerce_before_resend_order_emails' === $current_filter && ! empty( $_POST['woocommerce_meta_nonce'] ) && wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) && ! empty( $_POST['wc_order_action'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$action = wc_clean( $_POST['wc_order_action'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
self::$sending_downloadable_email = str_replace( 'send_email_', '', $action );
} else {
foreach ( self::$downloadable_email_data as $email_id => $hook_data ) {
if ( $current_filter == $hook_data['trigger_action'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
self::$sending_downloadable_email = $email_id;
}
}
}
}
/**
* Removes the downloadable email being sent flag. Hooked just after the email's trigger function.
*/
public static function remove_sending_downloadable_email_flag() {
self::$sending_downloadable_email = '';
}
/**
* Overrides the email order items template in recipient emails
*
* @param WC_Order $order Order object.
* @param array $args Email arguments.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function recipient_email_order_items_table( $order, $args ) {
$defaults = array(
'show_sku' => false,
'show_image' => false,
'image_size' => array( 32, 32 ),
'plain_text' => false,
'sent_to_admin' => false,
);
$args = wp_parse_args( $args, $defaults );
$template = $args['plain_text'] ? 'emails/plain/recipient-email-order-items.php' : 'emails/recipient-email-order-items.php';
wc_get_template(
$template,
array(
'order' => $order,
'items' => $order->get_items(),
'show_download_links' => $order->is_download_permitted() && ! $args['sent_to_admin'],
'show_sku' => $args['show_sku'],
'show_purchase_note' => $order->is_paid() && ! $args['sent_to_admin'],
'show_image' => $args['show_image'],
'image_size' => $args['image_size'],
'plain_text' => $args['plain_text'],
'sent_to_admin' => $args['sent_to_admin'],
),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
);
}
/**
* Show the order details table
*
* @param WC_Order $order Order object.
* @param bool $sent_to_admin Whether the email is sent to admin - defaults to false.
* @param bool $plain_text Whether the email should use plain text templates - defaults to false.
* @param WC_Email $email E-mail instance.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function order_details( $order, $sent_to_admin = false, $plain_text = false, $email = null ) {
$template_path = ( $plain_text ) ? 'emails/plain/recipient-email-order-details.php' : 'emails/recipient-email-order-details.php';
if ( wcs_is_subscription( $order ) ) {
// Translators: placeholder is a subscription ID.
$title = sprintf( _x( 'Subscription #%s', 'Used in email heading before line items table, placeholder is subscription ID', 'woocommerce-subscriptions' ), $order->get_order_number() );
} else {
// Translators: placeholder is an order ID.
$title = sprintf( _x( 'Order #%s', 'Used in email heading before line items table, placeholder is order ID', 'woocommerce-subscriptions' ), $order->get_order_number() );
}
wc_get_template(
$template_path,
array(
'order' => $order,
'sent_to_admin' => $sent_to_admin,
'plain_text' => $plain_text,
'email' => $email,
'title' => $title,
),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
);
}
/**
* Get the related subscription details table for emails sent to recipients.
*
* @param WC_Order $order The order object the email be sent relates to.
* @param bool $sent_to_admin Whether the email is sent to admin users.
* @param bool $plain_text Whether the email template is plain text or HTML.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function get_related_subscriptions_table( $order, $sent_to_admin, $plain_text ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
$template = ( $plain_text ) ? 'emails/plain/recipient-email-subscriptions-table.php' : 'emails/recipient-email-subscriptions-table.php';
// Only display the table if there are related subscriptions.
if ( ! empty( $subscriptions ) ) {
wc_get_template(
$template,
array( 'subscriptions' => $subscriptions ),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
);
}
}
/**
* Get the order's address details table for emails sent to recipients.
*
* @param WC_Order $order The order object the email be sent relates to.
* @param bool $sent_to_admin Whether the email is sent to admin users.
* @param bool $plain_text Whether the email template is plain text.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function get_address_table( $order, $sent_to_admin, $plain_text ) {
$template = ( $plain_text ) ? 'emails/plain/recipient-email-address-table.php' : 'emails/recipient-email-address-table.php';
wc_get_template(
$template,
array( 'order' => $order ),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
);
}
/**
* If a cart item contains recipient data matching the new customer, init the mailer and call the notification for new recipient customers.
*
* @param int $customer_id The ID of the new customer being created.
* @param array $new_customer_data Customer data.
* @param bool $password_generated Whether the password has been generated for the customer.
* @deprecated 2.0
*/
public static function send_new_recient_user_email( $customer_id, $new_customer_data, $password_generated ) {
_deprecated_function( __METHOD__, '2.0.0', __CLASS__ . '::send_new_recipient_user_email()' );
self::send_new_recipient_user_email( $customer_id, $new_customer_data, $password_generated );
}
}

View File

@@ -0,0 +1,213 @@
<?php
/**
* WooCommerce Memberships integration.
*
* @package WooCommerce Subscriptions Gifting/Integrations
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Implements integration with WooCommerce Memberships.
*/
class WCSG_Memberships_Integration {
/**
* Flag set when processing an order.
*
* @var mixed
*/
public static $processing_memberships_for_order;
/**
* Set up hooks and filters.
*/
public static function init() {
// Store the order id being processed so it can be used later.
add_action( 'woocommerce_order_status_completed', __CLASS__ . '::set_processing_memberships_for_order_flag', 1 );
add_action( 'woocommerce_order_status_processing', __CLASS__ . '::set_processing_memberships_for_order_flag', 1 );
add_action( 'woocommerce_order_status_completed', __CLASS__ . '::remove_processing_memberships_for_order_flag', 20 );
add_action( 'woocommerce_order_status_processing', __CLASS__ . '::remove_processing_memberships_for_order_flag', 20 );
// We need to hook late so other plugins don't override all our user unique order products.
add_filter( 'wc_memberships_access_granting_purchased_product_id', __CLASS__ . '::get_user_unique_membership_access_granting_product_ids', 100, 3 );
// We want to hook late so we don't override other plugins preventing granting membership.
add_filter( 'wc_memberships_grant_access_from_new_purchase', __CLASS__ . '::grant_membership_access', 100, 2 );
add_filter( 'wc_memberships_grant_access_from_existing_purchase', array( __CLASS__, 'grant_membership_access' ), 100, 2 );
// Set the correct subscription id stored on the membership. Called after Memberships has linked the subscription.
add_action( 'wc_memberships_grant_membership_access_from_purchase', __CLASS__ . '::update_subscription_id', 11, 2 );
}
/**
* Grants memberships to recipients and returns false so the purchaser is not granted the membership
* unless it is found that the purchaser also purchased the product for themselves.
*
* @param bool $grant_access Whether the membership will be granted with the following membership data.
* @param array $membership_data Array of data including: $user_id, $product_id, $order_id.
*/
public static function grant_membership_access( $grant_access, $membership_data ) {
if ( $grant_access && WCSG_Product::is_giftable( $membership_data['product_id'] ) && WCS_Gifting::order_contains_gifted_subscription( $membership_data['order_id'] ) ) {
// defaulted to false unless we find the purchaser has purchased the product for themselves.
$grant_access = false;
$order = wc_get_order( $membership_data['order_id'] );
$product_id = $membership_data['product_id'];
$user_id = $membership_data['user_id'];
$order_items = $order->get_items();
$grant_access_to_recipients = array();
foreach ( $order_items as $order_item_id => $order_item ) {
if ( $product_id && in_array( $product_id, array( $order_item['product_id'], $order_item['variation_id'] ) ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
if ( isset( $order_item['item_meta']['wcsg_recipient'] ) ) {
$grant_access_to_recipients[] = WCS_Gifting::get_order_item_recipient_user_id( $order_item );
} else {
$grant_access = true;
}
}
}
if ( ! empty( $grant_access_to_recipients ) ) {
foreach ( wc_memberships_get_membership_plans() as $plan ) {
if ( $plan->has_product( $product_id ) ) {
foreach ( $grant_access_to_recipients as $recipient_user_id ) {
$plan->grant_access_from_purchase( $recipient_user_id, $product_id, wcsg_get_objects_id( $order ) );
}
}
}
}
}
return $grant_access;
}
/**
* Sets a order id flag when processing an order so it can be later used inside
* self::get_user_unique_membership_access_granting_product_ids().
*
* @param int $order_id Order ID.
*/
public static function set_processing_memberships_for_order_flag( $order_id ) {
self::$processing_memberships_for_order = $order_id;
}
/**
* Removes the order id flag after memberships has processed the order.
*
* @param int $order_id Order ID.
*/
public static function remove_processing_memberships_for_order_flag( $order_id ) {
self::$processing_memberships_for_order = null;
}
/**
* By default memberships will determine what the best product in this order is to grant the membership
* (subscriptions with the longest end date take priority). However, because multiple subscriptions with
* multiple recipients (purchaser or gift recipient) is possible we need to get the best product per user.
*
* @param array $product_ids The product id(s) which will grant membership in this order.
* @param array $all_access_granting_product_ids Array of product IDs that can grant access to this plan.
* @param WC_Memberships_Membership_Plan $plan Membership plan access will be granted to.
*/
public static function get_user_unique_membership_access_granting_product_ids( $product_ids, $all_access_granting_product_ids, $plan ) {
$order = wc_get_order( self::$processing_memberships_for_order );
if ( WCS_Gifting::order_contains_gifted_subscription( $order ) ) {
$user_unique_product_ids = array();
$product_ids = array();
foreach ( $order->get_items() as $order_item_id => $order_item ) {
$user_id = ( isset( $order_item['item_meta']['wcsg_recipient'] ) ) ? WCS_Gifting::get_order_item_recipient_user_id( $order_item ) : $order->get_user_id();
if ( in_array( $order_item['product_id'], $all_access_granting_product_ids ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
$user_unique_product_ids[ $user_id ][] = $order_item['product_id'];
}
if ( in_array( $order_item['variation_id'], $all_access_granting_product_ids ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
$user_unique_product_ids[ $user_id ][] = $order_item['variation_id'];
}
}
remove_filter( 'wc_memberships_access_granting_purchased_product_id', __METHOD__, 100 );
foreach ( $user_unique_product_ids as $user_access_granting_product_ids ) {
$user_granting_product = ( 'yes' === get_option( 'wc_memberships_allow_cumulative_access_granting_orders', 'no' ) )
? $user_access_granting_product_ids
: $user_access_granting_product_ids[0];
$product_ids = array_unique( array_merge( $product_ids, (array) apply_filters( 'wc_memberships_access_granting_purchased_product_id', $user_granting_product, $user_access_granting_product_ids, $plan ) ) );
}
add_filter( 'wc_memberships_access_granting_purchased_product_id', __METHOD__, 100, 3 );
}
return $product_ids;
}
/**
* Because an order can contain multiple subscriptions with the same product in the one order we need
* to update the subscription linked to the membership.
* Gets the subscription the membership user has access to via recipient link.
*
* @param WC_Memberships_Membership_Plan $membership_plan The plan that user was granted access to.
* @param array $args Other arguments.
*/
public static function update_subscription_id( $membership_plan, $args ) {
$subscriptions_in_order = wcs_get_subscriptions(
array(
'order_id' => $args['order_id'],
'product_id' => $args['product_id'],
)
);
if ( ! empty( $subscriptions_in_order ) ) {
$order = wc_get_order( $args['order_id'] );
// Get the WC Memberships Subscription integration instance.
$wcm_subscriptions_integration_instance = is_callable( array( wc_memberships(), 'get_integrations_instance' ) )
? wc_memberships()->get_integrations_instance()->get_subscriptions_instance()
: wc_memberships()->get_subscriptions_integration();
// check if the member user is a recipient.
if ( $order->get_user_id() != $args['user_id'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$recipient_subscriptions = WCSG_Recipient_Management::get_recipient_subscriptions( $args['user_id'] );
$recipient_subscription_in_order = array_intersect( array_keys( $subscriptions_in_order ), $recipient_subscriptions );
$subscription = wcs_get_subscription( reset( $recipient_subscription_in_order ) );
if ( ! $subscription ) {
return;
}
update_post_meta( $args['user_membership_id'], '_subscription_id', wcsg_get_objects_id( $subscription ) );
// Update the membership end date to align it to the user's subscription.
$wcm_subscriptions_integration_instance->update_related_membership_dates( $subscription, 'end', $subscription->get_date( 'end' ) );
} else {
// If the member user is the purchaser, set the linked subscription to their subscription just in case.
foreach ( $subscriptions_in_order as $subscription ) {
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
if ( empty( $recipient_user_id ) ) {
update_post_meta( $args['user_membership_id'], '_subscription_id', wcsg_get_objects_id( $subscription ) );
$wcm_subscriptions_integration_instance->update_related_membership_dates( $subscription, 'end', $subscription->get_date( 'end' ) );
}
}
}
}
}
}
WCSG_Memberships_Integration::init();

View File

@@ -0,0 +1,149 @@
<?php
/**
* Integration with product pages on the frontend.
*
* @package WooCommerce Subscriptions Gifting
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class for integrating with product pages.
*/
class WCSG_Product {
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'woocommerce_add_cart_item_data', __CLASS__ . '::add_recipient_data', 1, 1 );
add_filter( 'woocommerce_get_cart_item_from_session', __CLASS__ . '::get_cart_items_from_session', 1, 2 );
add_filter( 'woocommerce_available_variation', __CLASS__ . '::add_gifting_to_variation_data', 10, 3 );
add_action( 'woocommerce_before_add_to_cart_button', __CLASS__ . '::add_gifting_option_product' );
}
/**
* Attaches recipient information to cart item data when a subscription is added to cart via product page.
* If the recipient email is invalid (incorrect email format or belongs to the current user) an exception is thrown
* and caught by WooCommerce add to cart function - preventing the product being entered into the cart.
*
* @param array $cart_item_data Cart item data.
* @return array New cart item data.
* @throws Exception In case of error.
*/
public static function add_recipient_data( $cart_item_data ) {
if ( isset( $_POST['recipient_email'] ) && ! empty( $_POST['recipient_email'][0] ) ) {
if ( ! empty( $_POST['_wcsgnonce'] ) && wp_verify_nonce( $_POST['_wcsgnonce'], 'wcsg_add_recipient' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( WCS_Gifting::validate_recipient_emails( wp_unslash( $_POST['recipient_email'] ) ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$cart_item_data['wcsg_gift_recipients_email'] = sanitize_email( wp_unslash( $_POST['recipient_email'][0] ) );
} else {
// throw exception to be caught by WC add_to_cart(). validate_recipient_emails() will have added the relevant notices.
throw new Exception();
}
} else {
throw new Exception( __( 'There was an error with your request. Please try again.', 'woocommerce-subscriptions' ) );
}
}
return $cart_item_data;
}
/**
* Adds the recipient information to the session cart item data.
*
* @param object $item The Session Data stored for an item in the cart.
* @param array $values The data stored on a cart item.
* @return object The session data with added cart item recipient information.
*/
public static function get_cart_items_from_session( $item, $values ) {
if ( array_key_exists( 'wcsg_gift_recipients_email', $values ) ) { // previously added at the product page via $cart_item_data.
$item['wcsg_gift_recipients_email'] = $values['wcsg_gift_recipients_email'];
unset( $values['wcsg_gift_recipients_email'] );
}
return $item;
}
/**
* Adds gifting ui elements to the subscription product page.
*/
public static function add_gifting_option_product() {
global $product;
if ( self::is_giftable( $product ) && ! isset( $_GET['switch-subscription'] ) ) {
$email = '';
if ( ! empty( $_POST['recipient_email'][0] ) && ! empty( $_POST['_wcsgnonce'] ) && wp_verify_nonce( $_POST['_wcsgnonce'], 'wcsg_add_recipient' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$email = $_POST['recipient_email'][0]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
}
WCS_Gifting::render_add_recipient_fields( $email );
}
}
/**
* Checks if a given product is a giftable product
*
* @param int|WC_Product $product A WC_Product object or the ID of a product to check.
* @return bool
*/
public static function is_giftable( $product ) {
if ( ! is_object( $product ) ) {
$product = wc_get_product( $product );
}
$is_giftable = false;
if ( WCSG_Admin::is_gifting_enabled() && WC_Subscriptions_Product::is_subscription( $product ) ) {
// On variable subscription products, it's always true to load the checkbox,
// let the variation handle showing the checkbox when it is giftable.
if ( WC_Subscriptions_Product::is_variable_subscription( $product ) ) {
$is_giftable = true;
} else {
// "Allow gifting" is set to "Enabled for all products".
$is_giftable = WCSG_Admin::is_gifting_enabled_for_all_products();
$product_gifting = WC_Subscriptions_Product::get_gifting( $product );
// Apply product-level override if it's set.
if ( '' !== $product_gifting ) {
$is_giftable = 'enabled' === $product_gifting;
}
}
}
/**
* Filter whether a product can be gifted.
*
* @param bool $is_giftable Whether the product can be gifted.
* @param WC_Product $product The product object.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.9.0.
*/
return apply_filters( 'wcsg_is_giftable_product', $is_giftable, $product );
}
/**
* Adds gifting data to the variation data.
* The variation data is used on the front-end as a value for the "found_variation" DOM event.
*
* @param array $variation_data The variation data.
* @param WC_Product $product The product object.
* @param WC_Product_Variation $variation The variation object.
* @return array The variation data with added gifting data.
*/
public static function add_gifting_to_variation_data( $variation_data, $product, $variation ) {
if ( ! WC_Subscriptions_Product::is_subscription( $product ) ) {
return $variation_data;
}
$variation_data['gifting'] = self::is_giftable( $variation );
return $variation_data;
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* Gifting's integration with `WCS_Query`.
*
* @package WooCommerce Subscriptions Gifting
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* WCSG_Query.
*/
class WCSG_Query extends WCS_Query {
/**
* Setup hooks & filters, when the class is constructed.
*/
public function __construct() {
add_action( 'init', array( $this, 'add_endpoints' ) );
add_filter( 'the_title', array( $this, 'change_endpoint_title' ), 11, 1 );
if ( ! is_admin() ) {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_filter( 'woocommerce_endpoint_new-recipient-account_title', array( $this, 'change_my_account_endpoint_title' ), 10, 2 );
}
$this->init_query_vars();
}
/**
* Init query vars by loading options.
*/
public function init_query_vars() {
$this->query_vars = array(
'new-recipient-account' => get_option( 'woocommerce_myaccount_view_subscriptions_endpoint', 'new-recipient-account' ),
);
}
/**
* Enqueue frontend scripts
*/
public function enqueue_scripts() {
if ( $this->is_query( 'new-recipient-account' ) ) {
// Enqueue WooCommerce country select scripts.
wp_enqueue_script( 'wc-country-select' );
wp_enqueue_script( 'wc-address-i18n' );
}
}
/**
* Changes the recipient account details endpoint title.
*
* Hooked onto the dynamic hook 'woocommerce_endpoint_new-recipient-account_title'.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.2.
*
* @param string $title Endpoint title.
* @param string $endpoint Endpoint.
*
* @return string The gift recipient account details page title.
*/
public function change_my_account_endpoint_title( $title, $endpoint ) {
return __( 'Account details', 'woocommerce-subscriptions' );
}
/* Function Overrides */
/**
* This function is attached to the 'woocommerce_account_menu_items' filter by the @see parent::__construct().
* In this context there is no menu items to add so this function is simply overriding the parent instance to avoid it from being called twice.
*
* @param array $menu_items The My Account menu items.
* @deprecated 2.0.0 Because parent::__construct() is no longer called, this function is no longer attached to any filters, no longer called and so no longer needs to be overridden.
*/
public function add_menu_items( $menu_items ) {
_deprecated_function( __METHOD__, '2.0' );
return $menu_items;
}
/**
* This function is attached to the 'woocommerce_account_subscriptions_endpoint' action hook by the @see parent::__construct().
* In this context there is no subscriptions endpoint content so this function is simply overriding the parent instance to avoid it from being called twice.
*
* @param int $current_page Current page.
* @deprecated 2.0.0 Because parent::__construct() is no longer called, this function is no longer attached to any hooks, no longer called and so no longer needs to be overridden.
*/
public function endpoint_content( $current_page = 1 ) {
_deprecated_function( __METHOD__, '2.0' );
}
}
new WCSG_Query();

View File

@@ -0,0 +1,91 @@
<?php
/**
* Address update handling.
*
* @package WooCommerce Subscriptions Gifting
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Allow for updating subscription addresses taking into consideration purchaser/recipient subscriptions.
*/
class WCSG_Recipient_Addresses {
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'wcs_get_users_subscriptions', __CLASS__ . '::get_users_subscriptions', 100, 2 );
add_filter( 'woocommerce_form_field_checkbox', __CLASS__ . '::display_update_all_addresses_notice', 1, 2 );
}
/**
* Returns the subset of user subscriptions which should be included when updating all subscription addresses.
* When setting shipping addresses only include those which the user has purchased for themselves or have been gifted to them.
* When setting billing addresses only include subscriptions that belong to the user and those they have gifted to another user.
*
* @param array $subscriptions Array of subscriptions.
* @param int $user_id User ID.
* @return array
*/
public static function get_users_subscriptions( $subscriptions, $user_id ) {
if ( ( 'shipping' === get_query_var( 'edit-address' ) || 'billing' === get_query_var( 'edit-address' ) ) && ! isset( $_GET['subscription'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// We dont want to update the shipping address of subscriptions the user isn't the recipient of.
if ( 'shipping' === get_query_var( 'edit-address' ) ) {
foreach ( $subscriptions as $subscription_id => $subscription ) {
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
if ( ! empty( $recipient_user_id ) && $recipient_user_id != $user_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
unset( $subscriptions[ $subscription_id ] );
}
}
} elseif ( 'billing' === get_query_var( 'edit-address' ) ) {
// We dont want to update the billing address of gifted subscriptions for this user.
foreach ( $subscriptions as $subscription_id => $subscription ) {
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
if ( ! empty( $recipient_user_id ) && $recipient_user_id == $user_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
unset( $subscriptions[ $subscription_id ] );
}
}
}
}
return $subscriptions;
}
/**
* Appends a notice to the 'update all subscriptions addresses' checkbox notifing the customer that updating all
* subscription addresses will not update gifted subscriptions, depending on which address is being updated.
*
* @param string $field The generated html element field string.
* @param string $field_id The id attribute of the html element being generated.
*/
public static function display_update_all_addresses_notice( $field, $field_id ) {
if ( 'update_all_subscriptions_addresses' === $field_id && ( 'shipping' === get_query_var( 'edit-address' ) || 'billing' === get_query_var( 'edit-address' ) ) ) {
switch ( get_query_var( 'edit-address' ) ) {
case 'shipping':
// Translators: 1) <strong> opening tag, 2) </strong> closing tag.
$field = substr_replace( $field, '<small>' . sprintf( esc_html__( '%1$sNote:%2$s This will not update the shipping address of subscriptions you have purchased for others.', 'woocommerce-subscriptions' ), '<strong>', '</strong>' ) . '</small>', strpos( $field, '</p>' ), 0 );
break;
case 'billing':
// Translators: 1) <strong> opening tag, 2) </strong> closing tag.
$field = substr_replace( $field, '<small>' . sprintf( esc_html__( '%1$sNote:%2$s This will not update the billing address of subscriptions purchased for you by someone else.', 'woocommerce-subscriptions' ), '<strong>', '</strong>' ) . '</small>', strpos( $field, '</p>' ), 0 );
break;
}
}
return $field;
}
}

View File

@@ -0,0 +1,310 @@
<?php
/**
* Recipient details endpoint.
*
* @package WooCommerce Subscriptions Gifting
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles the new recipient account endpoint.
*/
class WCSG_Recipient_Details {
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'template_redirect', __CLASS__ . '::update_recipient_details', 1 );
add_action( 'template_redirect', __CLASS__ . '::my_account_template_redirect' );
if ( wcsg_is_woocommerce_pre( '2.6' ) ) {
add_filter( 'wc_get_template', array( __CLASS__, 'add_new_customer_template' ), 10, 5 );
} else {
add_filter( 'wc_get_template', array( __CLASS__, 'get_new_recipient_account_container' ), 10, 4 );
add_action( 'woocommerce_account_new-recipient-account_endpoint', array( __CLASS__, 'get_new_customer_template' ) );
}
}
/**
* Determines if the current page is the recipient details page.
*
* @return boolean Whether the current page is the recipient details page or not.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
private static function is_recipient_details_page() {
global $wp;
return isset( $wp->query_vars['new-recipient-account'] );
}
/**
* Override the core My Account base template and display a full-width template if we're displaying the Recipient Details page.
*
* @param string $located Path to template.
* @param string $template_name The template's name.
* @param array $args An array of arguments used in the template.
* @param string $template_path Path for including template.
* @return string Path to template.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function get_new_recipient_account_container( $located, $template_name, $args, $template_path ) {
if ( 'myaccount/my-account.php' === $template_name && self::is_recipient_details_page() ) {
$located = wc_locate_template( 'recipient-details-my-account.php', $template_path, plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
}
return $located;
}
/**
* Get the new-recipient-account template
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function get_new_customer_template() {
wc_get_template( 'new-recipient-account.php', array(), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
}
/**
* Locates the new recipient details page template if the user is flagged for requiring further details.
*
* @param string $located Path to template.
* @param string $template_name The template's name.
* @param array $args An array of arguments used in the template.
* @param string $template_path Path for including template.
* @param string $default_path Default path.
* @return string Path to template.
*/
public static function add_new_customer_template( $located, $template_name, $args, $template_path, $default_path ) {
if ( 'myaccount/my-account.php' === $template_name && self::is_recipient_details_page() && 'true' === get_user_meta( get_current_user_id(), 'wcsg_update_account', true ) ) {
$located = wc_locate_template( 'new-recipient-account.php', $template_path, plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
}
return $located;
}
/**
* Redirects the user to the relevant page if they are trying to access my account or recipient account details page.
*/
public static function my_account_template_redirect() {
global $wp;
$current_user_id = get_current_user_id();
if ( is_account_page() && ! isset( $wp->query_vars['customer-logout'] ) ) {
if ( 'true' === get_user_meta( $current_user_id, 'wcsg_update_account', true ) && ! isset( $wp->query_vars['new-recipient-account'] ) ) {
wp_safe_redirect( wc_get_endpoint_url( 'new-recipient-account', '', wc_get_page_permalink( 'myaccount' ) ) );
exit();
} elseif ( 'true' !== get_user_meta( $current_user_id, 'wcsg_update_account', true ) && isset( $wp->query_vars['new-recipient-account'] ) ) {
wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
exit();
}
}
}
/**
* Validates the new recipient account details page updating user data and removing the 'required account update' user flag
* if there are no errors in validation.
*/
public static function update_recipient_details() {
if ( isset( $_POST['wcsg_new_recipient_customer'] ) && ! empty( $_POST['_wcsgnonce'] ) && wp_verify_nonce( $_POST['_wcsgnonce'], 'wcsg_new_recipient_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$country = ( ! empty( $_POST['shipping_country'] ) ) ? wc_clean( $_POST['shipping_country'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$form_fields = self::get_new_recipient_account_form_fields( $country );
$password_fields = array();
$password_missing = false;
foreach ( $form_fields as $key => $field ) {
if ( isset( $field['type'] ) && 'password' === $field['type'] ) {
$password_fields[ $key ] = $field;
}
// If the field is a required field and missing from posted data.
if ( isset( $field['required'] ) && true == $field['required'] && empty( $_POST[ $key ] ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
if ( isset( $password_fields[ $key ] ) ) {
if ( ! $password_missing ) {
wc_add_notice( __( 'Please enter both password fields.', 'woocommerce-subscriptions' ), 'error' );
$password_missing = true;
}
} else {
wc_add_notice( $field['label'] . ' ' . __( 'is a required field.', 'woocommerce-subscriptions' ), 'error' );
}
}
}
// Now match the passwords but only if we haven't displayed the password missing error.
if ( ! $password_missing && ! empty( $password_fields ) ) {
$passwords = array_intersect_key( $_POST, $password_fields );
if ( count( array_unique( $passwords ) ) !== 1 ) {
wc_add_notice( __( 'The passwords you have entered do not match.', 'woocommerce-subscriptions' ), 'error' );
}
}
// Validate the postcode field.
if ( ! empty( $_POST['shipping_postcode'] ) && ! WC_Validation::is_postcode( $_POST['shipping_postcode'], $_POST['shipping_country'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
wc_add_notice( __( 'Please enter a valid postcode/ZIP.', 'woocommerce-subscriptions' ), 'error' );
}
if ( 0 === wc_notice_count( 'error' ) ) {
$user = wp_get_current_user();
$address = array();
$non_user_meta_keys = array( 'set_billing' );
foreach ( $form_fields as $key => $field ) {
if ( ! in_array( $key, $non_user_meta_keys, true ) ) {
$value = isset( $_POST[ $key ] ) ? wc_clean( $_POST[ $key ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( false !== strpos( $key, 'shipping_' ) ) {
$address_field = str_replace( 'shipping_', '', $key ); // Get the key minus the leading 'shipping_'.
// If the field is a shipping first or last name and there isn't a posted value, fallback to our custom name field (if it exists).
if ( in_array( $key, array( 'shipping_first_name', 'shipping_last_name' ), true ) && empty( $_POST[ $key ] ) && ! empty( $_POST[ $address_field ] ) ) {
$value = wc_clean( $_POST[ $address_field ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
}
if ( isset( $_POST['set_billing'] ) ) {
update_user_meta( $user->ID, str_replace( 'shipping', 'billing', $key ), $value );
}
$address[ $address_field ] = $value;
}
update_user_meta( $user->ID, $key, $value );
}
}
// Find out the user's full name from our custom first/last name fields and the shipping fields (if available).
foreach ( array( 'first_name', 'last_name' ) as $name_property ) {
if ( empty( $_POST[ $name_property ] ) && ! empty( $_POST[ 'shipping_' . $name_property ] ) ) {
$user->{$name_property} = wc_clean( wp_unslash( $_POST[ 'shipping_' . $name_property ] ) );
}
}
if ( $user->first_name ) {
$user->nickname = $user->first_name;
$user->display_name = $user->first_name;
}
wp_update_user( $user );
if ( ! empty( $address ) ) {
$recipient_subscriptions = WCSG_Recipient_Management::get_recipient_subscriptions( $user->ID );
foreach ( $recipient_subscriptions as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
foreach( $address as $key => $value ) {
if ( is_callable( array( $subscription, 'set_shipping_' . $key ) ) ) {
$subscription->{'set_shipping_' . $key}( $value );
}
}
$subscription->save();
}
}
delete_user_meta( $user->ID, 'wcsg_update_account', 'true' );
delete_user_meta( $user->ID, 'wcsg_recipient_just_reset_password', 'true' );
do_action( 'wcsg_recipient_details_updated', $user->ID );
wc_add_notice( __( 'Your account has been updated.', 'woocommerce-subscriptions' ), 'notice' );
wp_safe_redirect( apply_filters( 'wcsg_recipient_details_update_redirect_url', wc_get_page_permalink( 'myaccount' ), $user->ID ) );
exit;
}
} elseif ( isset( $_POST['wcsg_new_recipient_customer'] ) ) {
wc_add_notice( __( 'There was an error with your request to update your account. Please try again.', 'woocommerce-subscriptions' ), 'error' );
}
}
/**
* Creates an array of form fields for the new recipient user details form
*
* @param string $country For which country we need to fetch fields.
* @param int $user_id For which user we need to fetch the fields. Default get_current_user_id().
*
* @return array Form elements for recipient details page
*/
public static function get_new_recipient_account_form_fields( $country, $user_id = null ) {
$user_id = ( is_numeric( $user_id ) ) ? absint( $user_id ) : get_current_user_id();
$shipping_fields = array();
if ( self::need_shipping_address_details_for_recipient( $user_id ) ) {
$shipping_fields = WC()->countries->get_address_fields( $country, 'shipping_' );
// We have our own name fields, so hide and make the shipping name fields not required.
foreach ( array( 'shipping_first_name', 'shipping_last_name' ) as $field_key ) {
$shipping_fields[ $field_key ]['type'] = 'hidden';
$shipping_fields[ $field_key ]['required'] = false;
$shipping_fields[ $field_key ]['label'] = '';
}
// Add the option for users to also set their billing address.
$shipping_fields['set_billing'] = array(
'type' => 'checkbox',
'label' => esc_html__( 'Set my billing address to the same as above.', 'woocommerce-subscriptions' ),
'class' => array( 'form-row' ),
'required' => false,
'default' => 1,
);
}
$personal_fields = array();
$user_requires_password = WCSG_Recipient_Management::user_requires_new_password( $user_id );
$personal_fields['first_name'] = array(
'label' => esc_html__( 'First Name', 'woocommerce-subscriptions' ),
'required' => true,
'class' => array( 'form-row-first' ),
'autocomplete' => 'given-name',
);
$personal_fields['last_name'] = array(
'label' => esc_html__( 'Last Name', 'woocommerce-subscriptions' ),
'required' => true,
'class' => array( 'form-row-last' ),
'clear' => true,
'autocomplete' => 'family-name',
);
return apply_filters( 'wcsg_new_recipient_account_details_fields', array_merge( $personal_fields, $shipping_fields ) );
}
/**
* Determines whether shipping address information is required for the given recipient.
*
* @param int $user_id User ID.
* @return bool TRUE if shipping address information is required for the given user.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.1.
*/
private static function need_shipping_address_details_for_recipient( $user_id = null ) {
$user_id = $user_id ? absint( $user_id ) : get_current_user_id();
if ( ! wc_shipping_enabled() ) {
return false;
}
$needs_shipping = WCS_Gifting::require_shipping_address_for_virtual_products();
if ( ! $needs_shipping ) {
foreach ( WCSG_Recipient_Management::get_recipient_subscriptions( $user_id ) as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( $subscription && $subscription->needs_shipping_address() ) {
$needs_shipping = true;
break;
}
}
}
return apply_filters( 'wcsg_need_shipping_address_details_for_recipient', $needs_shipping, $user_id );
}
}

View File

@@ -0,0 +1,684 @@
<?php
/**
* Recipient management.
*
* @package WooCommerce Subscriptions Gifting
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Recipient management class.
*/
class WCSG_Recipient_Management {
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'wcs_get_users_subscriptions', __CLASS__ . '::get_users_subscriptions', 1, 2 );
add_action( 'woocommerce_subscription_details_after_subscription_table', array( __CLASS__, 'gifting_information_after_customer_details' ), 1 );
add_filter( 'wcs_view_subscription_actions', __CLASS__ . '::add_recipient_actions', 11, 2 );
// We want to handle the changing of subscription status before Subscriptions core.
add_action( 'init', __CLASS__ . '::change_user_recipient_subscription', 99 );
add_filter( 'wcs_can_user_put_subscription_on_hold', __CLASS__ . '::recipient_can_suspend', 1, 2 );
if ( ! is_admin() ) {
add_filter( 'woocommerce_subscription_related_orders', array( __CLASS__, 'maybe_remove_parent_order' ), 11, 2 );
}
add_filter( 'user_has_cap', __CLASS__ . '::grant_recipient_capabilities', 20, 3 );
add_action( 'delete_user_form', __CLASS__ . '::maybe_display_delete_recipient_warning', 10 );
add_action( 'delete_user', __CLASS__ . '::maybe_remove_recipient', 10, 1 );
add_filter( 'woocommerce_attribute_label', __CLASS__ . '::format_recipient_meta_label', 10, 2 );
add_filter( 'woocommerce_order_item_display_meta_value', __CLASS__ . '::format_recipient_meta_value', 10 );
add_filter( 'woocommerce_hidden_order_itemmeta', __CLASS__ . '::hide_recipient_order_item_meta', 10, 1 );
add_action( 'woocommerce_before_order_itemmeta', __CLASS__ . '::display_recipient_meta_admin', 10, 1 );
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::maybe_update_recipient_role', 10, 2 );
// Hooked onto priority 8 for compatibility with WooCommerce Memberships.
add_action( 'woocommerce_order_status_completed', __CLASS__ . '::maybe_create_recipient', 8 );
add_action( 'woocommerce_order_status_processing', __CLASS__ . '::maybe_create_recipient', 8 );
if ( wcsg_is_woocommerce_pre( '3.0' ) ) {
add_action( 'woocommerce_add_order_item_meta', __CLASS__ . '::maybe_add_recipient_order_item_meta', 10, 2 );
}
add_action( 'woocommerce_customer_reset_password', array( __CLASS__, 'maybe_add_recipient_reset_password_flag' ) );
// Disable early renewal via modal for recipients.
add_action( 'template_redirect', array( __CLASS__, 'maybe_disable_early_renewal_modal_for_recipient' ), 100 );
}
/**
* Grant capabilities for subscriptions and related orders to recipients
*
* @param array $allcaps An array of user capabilities.
* @param array $caps The capability being questioned.
* @param array $args Additional arguments related to the capability.
* @return array
*/
public static function grant_recipient_capabilities( $allcaps, $caps, $args ) {
if ( isset( $caps[0] ) ) {
switch ( $caps[0] ) {
case 'view_order':
$user_id = $args[1];
$order = wc_get_order( $args[2] );
if ( $order ) {
if ( 'shop_subscription' === WC_Data_Store::load( 'subscription' )->get_order_type( $args[2] ) && WCS_Gifting::get_recipient_user( $order ) == $user_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$allcaps['view_order'] = true;
} elseif ( wcs_order_contains_renewal( $order ) ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
foreach ( $subscriptions as $subscription ) {
if ( WCS_Gifting::get_recipient_user( $subscription ) == $user_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$allcaps['view_order'] = true;
break;
}
}
}
}
break;
case 'pay_for_order':
$user_id = $args[1];
$order = wc_get_order( $args[2] );
if ( $order && wcs_order_contains_subscription( $order, 'any' ) ) {
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
foreach ( $subscriptions as $subscription ) {
if ( WCS_Gifting::get_recipient_user( $subscription ) == $user_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$allcaps['pay_for_order'] = true;
break;
}
}
}
break;
case 'subscribe_again':
// subscribe_again is the capability used to enable resubscription. Recipients cannot resubscribe (@see https://docs.woocommerce.com/document/subscriptions-gifting/#section-13) and so we only want to enable this function for early renewals.
if ( ! isset( $_GET['subscription_renewal_early'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
break;
}
$user_id = $args[1];
$subscription = wcs_get_subscription( $args[2] );
if ( WCS_Gifting::is_gifted_subscription( $subscription ) && (int) WCS_Gifting::get_recipient_user( $subscription ) === $user_id ) {
$allcaps['subscribe_again'] = true;
}
break;
}
}
return $allcaps;
}
/**
* Adds available user actions to the subscription recipient
*
* @param array $actions An array of actions the user can peform.
* @param object $subscription Subscription object.
* @return array An updated array of actions the user can perform on a gifted subscription.
*/
public static function add_recipient_actions( $actions, $subscription ) {
if ( WCS_Gifting::is_gifted_subscription( $subscription ) && get_current_user_id() == WCS_Gifting::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$recipient_actions = array();
$current_status = $subscription->get_status();
$recipient_user = wp_get_current_user();
$subscription_id = wcsg_get_objects_id( $subscription );
$admin_with_suspension_disallowed = ( current_user_can( 'manage_woocommerce' ) && '0' === get_option( WC_Subscriptions_Admin::$option_prefix . '_max_customer_suspensions', '0' ) ) ? true : false;
if ( $subscription->can_be_updated_to( 'on-hold' ) && wcs_can_user_put_subscription_on_hold( $subscription, $recipient_user ) && ! $admin_with_suspension_disallowed ) {
$recipient_actions['suspend'] = array(
'url' => self::get_recipient_change_status_link( $subscription_id, 'on-hold', $recipient_user->ID, $current_status ),
'name' => __( 'Suspend', 'woocommerce-subscriptions' ),
);
} elseif ( $subscription->can_be_updated_to( 'active' ) && ! $subscription->needs_payment() ) {
$recipient_actions['reactivate'] = array(
'url' => self::get_recipient_change_status_link( $subscription_id, 'active', $recipient_user->ID, $current_status ),
'name' => __( 'Reactivate', 'woocommerce-subscriptions' ),
);
}
if ( $subscription->can_be_updated_to( 'cancelled' ) ) {
$recipient_actions['cancel'] = array(
'url' => self::get_recipient_change_status_link( $subscription_id, 'cancelled', $recipient_user->ID, $current_status ),
'name' => __( 'Cancel', 'woocommerce-subscriptions' ),
);
}
$actions = array_merge( $recipient_actions, $actions );
// Remove the ability for recipients to change the payment method.
unset( $actions['change_payment_method'] );
}
return $actions;
}
/**
* Disables the early renewal modal for recipients. This is to prevent recipients from renewing using the purchaser's
* payment information.
* This is only required when running on WCS < 3.0.5, where being able to renew early implies access to the early renewal modal.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.1.
*/
public static function maybe_disable_early_renewal_modal_for_recipient() {
if ( ! wcsg_is_wc_subscriptions_pre( '3.0.5' ) ) {
return;
}
if ( ! wcs_is_view_subscription_page() || ! isset( $GLOBALS['wp']->query_vars['view-subscription'] ) ) {
return;
}
$subscription = wcs_get_subscription( absint( $GLOBALS['wp']->query_vars['view-subscription'] ) );
if ( ! $subscription || ! WCS_Gifting::is_gifted_subscription( $subscription ) || get_current_user_id() === $subscription->get_user_id() ) {
return;
}
$callback = array( 'WCS_Early_Renewal_Modal_Handler', 'maybe_print_early_renewal_modal' );
remove_action( 'woocommerce_subscription_details_table', $callback, has_action( 'woocommerce_subscription_details_table', $callback ) );
}
/**
* Generates a link for the user to change the status of a subscription
*
* @param int $subscription_id Subscription ID.
* @param string $status The status the recipient has requested to change the subscription to.
* @param int $recipient_id Recipient ID.
* @param string $current_status Current status.
*/
private static function get_recipient_change_status_link( $subscription_id, $status, $recipient_id, $current_status ) {
$action_link = add_query_arg(
array(
'subscription_id' => $subscription_id,
'change_subscription_to' => $status,
'wcsg_requesting_recipient_id' => $recipient_id,
)
);
$action_link = wp_nonce_url( $action_link, $subscription_id . $current_status );
return $action_link;
}
/**
* Checks if a status change request is by the recipient, and if it is,
* validate the request and proceed to change to the subscription.
*/
public static function change_user_recipient_subscription() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// Check if the request is being made from the recipient (wcsg_requesting_recipient_id is set).
if ( isset( $_GET['wcsg_requesting_recipient_id'] ) && isset( $_GET['change_subscription_to'] ) && isset( $_GET['subscription_id'] ) && ! empty( $_GET['_wpnonce'] ) ) {
remove_action( 'init', 'WCS_User_Change_Status_Handler::maybe_change_users_subscription', 100 );
$subscription = wcs_get_subscription( absint( $_GET['subscription_id'] ) );
$user_id = $subscription->get_user_id();
$new_status = $_GET['change_subscription_to']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( WCS_User_Change_Status_Handler::validate_request( $user_id, $subscription, $new_status, $_GET['_wpnonce'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
WCS_User_Change_Status_Handler::change_users_subscription( $subscription, $new_status );
wp_safe_redirect( $subscription->get_view_order_url() );
exit;
}
}
// phpcs:enable
}
/**
* Allows the recipient to suspend a subscription, provided the suspension count hasnt been reached
*
* @param bool $user_can_suspend Whether the user can suspend a subscription.
* @param WC_Subscription $subscription Subscription object.
*/
public static function recipient_can_suspend( $user_can_suspend, $subscription ) {
if ( WCS_Gifting::is_gifted_subscription( $subscription ) && get_current_user_id() == WCS_Gifting::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
// Make sure subscription suspension count hasn't been reached.
$suspension_count = wcsg_get_objects_property( $subscription, 'suspension_count' );
$allowed_suspensions = get_option( WC_Subscriptions_Admin::$option_prefix . '_max_customer_suspensions', 0 );
if ( 'unlimited' === $allowed_suspensions || $allowed_suspensions > $suspension_count ) { // 0 not > anything so prevents a customer ever being able to suspend
$user_can_suspend = true;
}
}
return $user_can_suspend;
}
/**
* Adds all the subscriptions that have been gifted to a user to their subscriptions
*
* @param array $subscriptions An array of subscriptions assigned to the user.
* @param int $user_id Recipient's user ID.
* @return array An updated array of subscriptions with any subscriptions gifted to the user added.
*/
public static function get_users_subscriptions( $subscriptions, $user_id ) {
// Get the subscription posts that have been gifted to this user.
$recipient_subscriptions = self::get_recipient_subscriptions( $user_id );
foreach ( $recipient_subscriptions as $subscription_id ) {
$subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id );
}
if ( 0 < count( $recipient_subscriptions ) ) {
krsort( $subscriptions );
}
return $subscriptions;
}
/**
* Adds recipient/purchaser information to the view subscription page.
*
* @param WC_Subscription $subscription Subscription object.
*/
public static function gifting_information_after_customer_details( $subscription ) {
// check if the subscription is gifted.
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
$customer_user_id = $subscription->get_user_id();
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
$current_user_id = get_current_user_id();
if ( $current_user_id == $customer_user_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
wc_get_template(
'html-view-subscription-gifting-information.php',
array(
'user_title' => 'Recipient',
'name' => WCS_Gifting::get_user_display_name( $recipient_user_id ),
),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
);
} else {
wc_get_template(
'html-view-subscription-gifting-information.php',
array(
'user_title' => 'Purchaser',
'name' => WCS_Gifting::get_user_display_name( $customer_user_id ),
),
'',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/'
);
}
}
}
/**
* Gets an array of subscription ids which have been gifted to a user
*
* @param int $user_id The user id of the recipient.
* @param int $order_id The Order ID which contains the subscription.
* @param array $args Array of arguments.
*
* @return int[] An array of subscription IDs gifted to the user
*/
public static function get_recipient_subscriptions( $user_id, $order_id = 0, $args = array() ) {
$args = wp_parse_args(
$args,
array(
'subscriptions_per_page' => -1,
'subscription_status' => 'any',
'orderby' => 'start_date',
'order' => 'DESC',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_recipient_user',
'value' => $user_id,
'compare' => '=',
),
),
)
);
if ( 0 != $order_id ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$args['order_id'] = $order_id;
}
return array_keys( wcs_get_subscriptions( $args ) );
}
/**
* Filter the WC_Subscription::get_related_orders() method removing parent orders for recipients.
*
* @param array $related_orders An array of order ids related to the $subscription.
* @param WC_Subscription $subscription Subscription object.
* @return array an array of order ids related to the $subscription.
*/
public static function maybe_remove_parent_order( $related_orders, $subscription ) {
if ( WCS_Gifting::is_gifted_subscription( $subscription ) && get_current_user_id() == WCS_Gifting::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$parent_order_id = $subscription->get_parent_id();
if ( isset( $related_orders[ $parent_order_id ] ) ) {
unset( $related_orders[ $parent_order_id ] );
}
}
return $related_orders;
}
/**
* Maybe add recipient information to order item meta for displaying in order item tables.
*
* @param int $item_id The item ID.
* @param array $cart_item The cart's item.
*/
public static function maybe_add_recipient_order_item_meta( $item_id, $cart_item ) {
$recipient_user_id = WCSG_Cart::get_recipient_from_cart_item( $cart_item );
if ( $recipient_user_id ) {
wc_update_order_item_meta( $item_id, 'wcsg_recipient', 'wcsg_recipient_id_' . $recipient_user_id );
// Clear the order item meta cache so all meta is included in emails sent on checkout.
$cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'item_meta_array_' . $item_id;
wp_cache_delete( $cache_key, 'orders' );
}
}
/**
* Format the order item meta label to be displayed.
*
* @param string $label The item meta label displayed.
* @param string $name The name of the order item meta (key).
*/
public static function format_recipient_meta_label( $label, $name ) {
if ( 'wcsg_recipient' === $name || 'wcsg_deleted_recipient_data' === $name ) {
$label = __( 'Recipient', 'woocommerce-subscriptions' );
}
return $label;
}
/**
* Format recipient order item meta value by extracting the recipient user id.
*
* @param mixed $value Order item meta value.
*/
public static function format_recipient_meta_value( $value ) {
if ( false !== strpos( $value, 'wcsg_recipient_id' ) ) {
$recipient_id = substr( $value, strlen( 'wcsg_recipient_id_' ) );
$strip_tags = is_checkout() && ! is_wc_endpoint_url( 'order-received' );
return WCS_Gifting::get_user_display_name( $recipient_id, $strip_tags );
} elseif ( false !== strpos( $value, 'wcsg_deleted_recipient_data' ) ) {
$recipient_data = json_decode( substr( $value, strlen( 'wcsg_deleted_recipient_data_' ) ), true );
return $recipient_data['display_name'];
}
return $value;
}
/**
* Prevents default display of recipient meta in admin panel.
*
* @param array $ignored_meta_keys An array of order item meta keys which are skipped when displaying meta.
*/
public static function hide_recipient_order_item_meta( $ignored_meta_keys ) {
array_push( $ignored_meta_keys, 'wcsg_recipient', 'wcsg_deleted_recipient_data' );
return $ignored_meta_keys;
}
/**
* Displays recipient order item meta for admin panel.
*
* @param int $item_id The id of the order item.
*/
public static function display_recipient_meta_admin( $item_id ) {
$recipient_meta = wc_get_order_item_meta( $item_id, 'wcsg_recipient' );
$deleted_recipient_meta = wc_get_order_item_meta( $item_id, 'wcsg_deleted_recipient_data' );
$recipient_shipping_address = '';
$recipient_display_name = '';
if ( ! empty( $recipient_meta ) ) {
$recipient_id = substr( $recipient_meta, strlen( 'wcsg_recipient_id_' ) );
$recipient_shipping_address = WC()->countries->get_formatted_address( WCS_Gifting::get_users_shipping_address( $recipient_id ) );
$recipient_display_name = WCS_Gifting::get_user_display_name( $recipient_id );
} elseif ( ! empty( $deleted_recipient_meta ) ) {
$recipient_data = json_decode( substr( $deleted_recipient_meta, strlen( 'wcsg_deleted_recipient_data_' ) ), true );
$recipient_display_name = $recipient_data['display_name'];
unset( $recipient_data['display_name'] );
$recipient_shipping_address = WC()->countries->get_formatted_address( $recipient_data );
}
if ( ! empty( $recipient_meta ) || ! empty( $deleted_recipient_meta ) ) {
if ( empty( $recipient_shipping_address ) ) {
$recipient_shipping_address = 'N/A';
}
$tooltip_content = '';
$tooltip_content .= __( 'Shipping:', 'woocommerce-subscriptions' );
$tooltip_content .= ' ';
$tooltip_content .= $recipient_shipping_address;
echo '<br />';
echo '<b>' . esc_html__( 'Recipient:', 'woocommerce-subscriptions' ) . '</b> ' . wp_kses( $recipient_display_name, wp_kses_allowed_html( 'user_description' ) );
echo '<img class="help_tip" data-tip="' . wc_sanitize_tooltip( $tooltip_content ) . '" src="' . esc_url( WC()->plugin_url() ) . '/assets/images/help.png" height="16" width="16" />'; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
}
}
/**
* Removes recipient subscription meta from gifted subscriptions if the recipient is deleted.
*
* @param int $user_id The id of the user being deleted.
*/
public static function maybe_remove_recipient( $user_id ) {
$gifted_subscriptions = self::get_recipient_subscriptions( $user_id );
$gifted_items = WCS_Gifting::get_recipient_order_items( $user_id );
if ( ! empty( $gifted_subscriptions ) ) {
foreach ( $gifted_subscriptions as $subscription_id ) {
WCS_Gifting::set_recipient_user( $subscription, 'deleted_recipient' );
}
$recipient = get_user_by( 'id', $user_id );
$recipient_data = wp_json_encode(
array_merge(
array( 'display_name' => addslashes( WCS_Gifting::get_user_display_name( $user_id ) ) ),
WCS_Gifting::get_users_shipping_address( $user_id )
)
);
foreach ( $gifted_items as $gifted_item ) {
if ( ! wcs_is_subscription( $gifted_item['order_id'] ) ) {
wc_update_order_item_meta( $gifted_item['order_item_id'], 'wcsg_deleted_recipient_data', 'wcsg_deleted_recipient_data_' . $recipient_data );
}
wc_delete_order_item_meta( $gifted_item['order_item_id'], 'wcsg_recipient', 'wcsg_recipient_id_' . $user_id );
}
}
}
/**
* Displays a warning message if a recipient is in the process of being deleted.
*/
public static function maybe_display_delete_recipient_warning() {
$recipient_users = array();
$user_ids = array();
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( empty( $_REQUEST['users'] ) && ! empty( $_REQUEST['user'] ) ) {
$user_ids = array( $_REQUEST['user'] );
} else {
$user_ids = $_REQUEST['users'];
}
// phpcs:enable
if ( ! empty( $user_ids ) ) {
foreach ( $user_ids as $user_id ) {
$gifted_subscriptions = self::get_recipient_subscriptions( $user_id );
if ( 0 !== count( $gifted_subscriptions ) ) {
$recipient_users[ $user_id ] = $gifted_subscriptions;
}
}
$recipients_count = count( $recipient_users );
if ( 0 !== $recipients_count ) {
echo '<p><strong>' . esc_html__( 'WARNING:', 'woocommerce-subscriptions' ) . ' </strong>';
echo esc_html( _n( 'The following recipient will be removed from their subscriptions:', 'The following recipients will be removed from their subscriptions:', $recipients_count, 'woocommerce-subscriptions' ) );
echo '<p><dl>';
foreach ( $recipient_users as $recipient_id => $subscriptions ) {
$recipient = get_userdata( $recipient_id );
echo '<dt>ID #' . esc_attr( $recipient_id ) . ': ' . esc_attr( $recipient->user_login ) . '</dt>';
foreach ( $subscriptions as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
echo '<dd>' . esc_html__( 'Subscription', 'woocommerce-subscriptions' ) . ' <a href="' . esc_url( wcs_get_edit_post_link( wcsg_get_objects_id( $subscription ) ) ) . '">#' . esc_html( $subscription->get_order_number() ) . '</a></dd>';
}
}
echo '</dl>';
}
}
}
/**
* On password reset, if the user needs to update account, sets a (temporary) flag
*
* @param WP_User $user User object.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
public static function maybe_add_recipient_reset_password_flag( $user ) {
if ( 'true' === get_user_meta( $user->ID, 'wcsg_update_account', true ) ) {
update_user_meta( $user->ID, 'wcsg_recipient_just_reset_password', 'true' );
}
}
/**
* Does the user require a new password after password reset?
*
* @param Int $user_id User ID we want to validate.
*
* @return bool
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.1.0.
*/
public static function user_requires_new_password( $user_id ) {
return ! metadata_exists( 'user', $user_id, 'wcsg_recipient_just_reset_password' );
}
/**
* On subscription status changes, maybe update the role of the subscription recipient (if set) depending on the new subscription status.
* Sets the recipient user to the inactive subscriber role on on-hold, cancelled, expired statuses and an active subscriber role on active statuses.
*
* @param WC_Subscription $subscription Subscription object.
* @param string $new_status The subscription's new status.
*/
public static function maybe_update_recipient_role( $subscription, $new_status ) {
$inactive_statuses = array(
'on-hold',
'cancelled',
'expired',
);
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
$recipient_user_id = WCS_Gifting::get_recipient_user( $subscription );
if ( in_array( $new_status, $inactive_statuses, true ) ) {
wcs_maybe_make_user_inactive( $recipient_user_id );
} elseif ( 'active' === $new_status ) {
wcs_make_user_active( $recipient_user_id );
}
}
}
/**
* When orders are processed/completed, create new recipients and attach shipping information to gifted subscriptions.
*
* @param int $order_id Order ID.
*/
public static function maybe_create_recipient( $order_id ) {
$subscriptions = wcs_get_subscriptions_for_order( $order_id );
foreach( $subscriptions as $subscription ) {
self::maybe_create_recipient_and_attach_shipping_information( $subscription );
}
}
/**
* Maybe create a recipient user and attach shipping information to a subscription.
*
* @param WC_Subscription $subscription The subscription object.
*/
public static function maybe_create_recipient_and_attach_shipping_information( $subscription ) {
$recipient_user_email_address = $subscription->get_meta( '_recipient_user_email_address' );
if ( ! $recipient_user_email_address ) {
return;
}
$recipient_user_id = email_exists( $recipient_user_email_address );
// Create a new user if the recipient's email doesn't already exist.
if ( ! $recipient_user_id ) {
$recipient_user_id = WCS_Gifting::create_recipient_user( $recipient_user_email_address );
}
if ( ! is_numeric( $recipient_user_id ) ) {
return;
}
WCS_Gifting::set_recipient_user( $subscription, $recipient_user_id );
$subscription->set_shipping_first_name( get_user_meta( $recipient_user_id, 'shipping_first_name', true ) );
$subscription->set_shipping_last_name( get_user_meta( $recipient_user_id, 'shipping_last_name', true ) );
$subscription->set_shipping_company( get_user_meta( $recipient_user_id, 'shipping_company', true ) );
$subscription->set_shipping_address_1( get_user_meta( $recipient_user_id, 'shipping_address_1', true ) );
$subscription->set_shipping_address_2( get_user_meta( $recipient_user_id, 'shipping_address_2', true ) );
$subscription->set_shipping_city( get_user_meta( $recipient_user_id, 'shipping_city', true ) );
$subscription->set_shipping_state( get_user_meta( $recipient_user_id, 'shipping_state', true ) );
$subscription->set_shipping_postcode( get_user_meta( $recipient_user_id, 'shipping_postcode', true ) );
$subscription->set_shipping_country( get_user_meta( $recipient_user_id, 'shipping_country', true ) );
$subscription->save();
}
/**
* Attach WooCommerce version dependent hooks
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
* @deprecated 2.0.0
*/
public static function attach_dependant_hooks() {
_deprecated_function( __METHOD__, '2.0.0', __CLASS__ . '::init()' );
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* WCS Gifting Template Loader
*
* @package WooCommerce Subscriptions Gifting
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Locates Gifting templates for use through `wc_get_template()`.
*/
class WCSG_Template_Loader {
/**
* Setup hooks & filters, when the class is initialised.
*/
public static function init() {
add_filter( 'wc_get_template', array( __CLASS__, 'get_recent_orders_template' ), 1, 3 );
add_filter( 'wc_get_template', array( __CLASS__, 'get_subscription_totals_template' ), 1, 3 );
add_filter( 'wc_get_template', array( __CLASS__, 'get_customer_details_template' ), 1, 3 );
}
/**
* Overrides the default recent order template for gifted subscriptions
*
* @param string $located Path to template.
* @param string $template_name Template name.
* @param array $args Arguments.
* @return string Path for including template.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function get_recent_orders_template( $located, $template_name, $args ) {
if ( 'myaccount/related-orders.php' === $template_name ) {
$subscription = $args['subscription'];
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
$located = wc_locate_template( 'related-orders.php', '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
}
}
return $located;
}
/**
* Overrides subscription totals template.
*
* @param string $located Path to template.
* @param string $template_name Template name.
* @param array $args Arguments.
* @return string Path for including template.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function get_subscription_totals_template( $located, $template_name, $args ) {
if ( ! wcsg_is_wc_subscriptions_pre( '2.2.19' ) && 'myaccount/subscription-totals.php' === $template_name ) {
$subscription = $args['subscription'];
if ( WCS_Gifting::is_gifted_subscription( $subscription ) && get_current_user_id() == WCS_Gifting::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$located = wc_locate_template( 'subscription-totals.php', '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
}
}
return $located;
}
/**
* Overrides the order details customer template on view subscription page for recipient.
*
* @param string $located Path to template.
* @param string $template_name Template name.
* @param array $args Arguments.
* @return string Path for including template.
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
public static function get_customer_details_template( $located, $template_name, $args ) {
if ( 'order/order-details-customer.php' === $template_name && isset( $args['order'] ) && ! wcsg_is_wc_subscriptions_pre( '2.2.19' ) ) {
$subscription = $args['order'];
if ( WCS_Gifting::is_gifted_subscription( $subscription ) && get_current_user_id() == WCS_Gifting::get_recipient_user( $subscription ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$located = wc_locate_template( 'order-details-customer.php', '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/' );
}
}
return $located;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* E-mails: Completed renewal order.
*
* @package WooCommerce Subscriptions Gifting/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles e-mailing of the "Completed Renewal Order" e-mail to recipients.
*/
class WCSG_Email_Completed_Renewal_Order extends WCS_Email_Completed_Renewal_Order {
/**
* Recipient's user ID.
*
* @var int
*/
public $wcsg_sending_recipient_email;
/**
* Create an instance of the class.
*/
public function __construct() {
$this->id = 'recipient_completed_renewal_order';
$this->title = __( 'Completed Renewal Order - Recipient', 'woocommerce-subscriptions' );
$this->description = __( 'Renewal order complete emails are sent to the recipient when a subscription renewal order is marked complete and usually indicates that the item for that renewal period has been shipped.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'Your renewal order is complete', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {blogname} renewal order from {order_date} is complete', 'woocommerce-subscriptions' );
$this->template_html = 'emails/recipient-completed-renewal-order.php';
$this->template_plain = 'emails/plain/recipient-completed-renewal-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/';
add_action( 'woocommerce_order_status_completed_renewal_notification_recipient', array( $this, 'trigger' ) );
WC_Email::__construct();
}
/**
* Trigger function.
*
* @param int $order_id Order ID.
* @param object $order Order object.
*/
public function trigger( $order_id, $order = null ) {
$recipient_id = null;
if ( $order_id ) {
$this->object = wc_get_order( $order_id );
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
$subscriptions = array_values( $subscriptions );
$recipient_id = WCS_Gifting::get_recipient_user( wcs_get_subscription( $subscriptions[0] ) );
$this->recipient = get_userdata( $recipient_id )->user_email;
}
$order_date_index = array_search( '{order_date}', $this->find, true );
$date_format = is_callable( 'wc_date_format' ) ? wc_date_format() : woocommerce_date_format();
$order_date_time = is_callable( array( $this->object, 'get_date_created' ) ) ? $this->object->get_date_created()->getTimestamp() : strtotime( $this->object->order_date );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( $date_format, $order_date_time );
} else {
$this->replace[ $order_date_index ] = date_i18n( $date_format, $order_date_time );
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->wcsg_sending_recipient_email = $recipient_id;
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
unset( $this->wcsg_sending_recipient_email );
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* E-mails: Customer New Account.
*
* @package WooCommerce Subscriptions Gifting/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles e-mailing to purchaser of new account notification.
*/
class WCSG_Email_Customer_New_Account extends WC_Email {
/**
* Subscription purchaser's name.
*
* @var string
*/
public $subscription_owner;
/**
* Recipient's user name.
*
* @var string
*/
public $user_login;
/**
* Recipient's e-mail address.
*
* @var string
*/
public $user_email;
/**
* Recipient's user ID.
*
* @var int
*/
public $user_id;
/**
* Recipient's account reset key.
*
* @var string
*/
public $reset_key;
/**
* Create an instance of the class.
*/
public function __construct() {
// Call override values.
$this->id = 'WCSG_Email_Customer_New_Account';
$this->title = __( 'New Recipient Account', 'woocommerce-subscriptions' );
$this->description = __( 'New account notification emails are sent to the subscription recipient when an account is created for them.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->subject = __( 'Your account on {site_title}', 'woocommerce-subscriptions' );
$this->heading = __( 'Welcome to {site_title}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/new-recipient-customer.php';
$this->template_plain = 'emails/plain/new-recipient-customer.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/';
// Triggers for this email.
add_action( 'wcsg_created_customer_notification', array( $this, 'trigger' ), 10, 3 );
WC_Email::__construct();
}
/**
* Trigger function.
*
* @param int $user_id User ID.
* @param string $reset_key Reset key.
* @param string $subscription_purchaser Purchaser's name.
*/
public function trigger( $user_id, $reset_key, $subscription_purchaser ) {
if ( $user_id ) {
$this->object = get_user_by( 'id', $user_id );
$this->reset_key = $reset_key;
$this->user_login = stripslashes( $this->object->user_login );
$this->user_email = stripslashes( $this->object->user_email );
$this->user_id = $user_id;
$this->recipient = $this->user_email;
$this->subscription_owner = $subscription_purchaser;
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
/**
* Returns content for the HTML version of the e-mail.
*/
public function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'email_heading' => $this->get_heading(),
'user_login' => $this->user_login,
'user_id' => $this->user_id,
'reset_key' => $this->reset_key,
'blogname' => $this->get_blogname(),
'subscription_purchaser' => $this->subscription_owner,
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
'additional_content' => $this->get_additional_content(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* Returns content for the plain text version of the e-mail.
*/
public function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'email_heading' => $this->get_heading(),
'user_login' => $this->user_login,
'user_id' => $this->user_id,
'reset_key' => $this->reset_key,
'blogname' => $this->get_blogname(),
'subscription_purchaser' => $this->subscription_owner,
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
'additional_content' => $this->get_additional_content(),
),
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* E-mails: Processing renewal order.
*
* @package WooCommerce Subscriptions Gifting/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles e-mailing of the "Processing Renewal Order" e-mail to recipients.
*/
class WCSG_Email_Processing_Renewal_Order extends WCS_Email_Processing_Renewal_Order {
/**
* Recipient user ID.
*
* @var int
*/
public $wcsg_sending_recipient_email;
/**
* Create an instance of the class.
*/
public function __construct() {
$this->id = 'gift_recipient_processing_renewal_order';
$this->title = __( 'Processing Renewal Order - Recipient', 'woocommerce-subscriptions' );
$this->description = __( 'This is an order notification sent to the recipient after payment for a subscription renewal order is completed. It contains the renewal order details.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'Thank you for your order', 'woocommerce-subscriptions' );
$this->subject = __( 'Your {blogname} renewal order receipt from {order_date}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/recipient-processing-renewal-order.php';
$this->template_plain = 'emails/plain/recipient-processing-renewal-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/';
add_action( 'woocommerce_order_status_pending_to_processing_renewal_notification_recipient', array( $this, 'trigger' ) );
add_action( 'woocommerce_order_status_pending_to_on-hold_renewal_notification_recipient', array( $this, 'trigger' ) );
WC_Email::__construct();
}
/**
* Trigger function.
*
* @param int $order_id Order ID.
* @param WC_Order|null $order Order object.
*/
public function trigger( $order_id, $order = null ) {
$recipient_id = null;
if ( $order_id ) {
$this->object = wc_get_order( $order_id );
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
$subscriptions = array_values( $subscriptions );
$recipient_id = WCS_Gifting::get_recipient_user( wcs_get_subscription( $subscriptions[0] ) );
$this->recipient = get_user_by( 'id', $recipient_id )->user_email;
}
$order_date_index = array_search( '{order_date}', $this->find, true );
$date_format = is_callable( 'wc_date_format' ) ? wc_date_format() : woocommerce_date_format();
$order_date_time = is_callable( array( $this->object, 'get_date_created' ) ) ? $this->object->get_date_created()->getTimestamp() : strtotime( $this->object->order_date );
if ( false === $order_date_index ) {
$this->find[] = '{order_date}';
$this->replace[] = date_i18n( $date_format, $order_date_time );
} else {
$this->replace[ $order_date_index ] = date_i18n( $date_format, $order_date_time );
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->wcsg_sending_recipient_email = $recipient_id;
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
unset( $this->wcsg_sending_recipient_email );
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* E-mails: New initial order.
*
* @package WooCommerce Subscriptions Gifting/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles e-mailing of the "New Initial Order" e-mail to recipients.
*/
class WCSG_Email_Recipient_New_Initial_Order extends WC_Email {
/**
* Subscription owner name.
*
* @var string
*/
public $subscription_owner;
/**
* Array of subscription post objects.
*
* @var WP_Post[]
*/
public $subscriptions;
/**
* Recipient user ID.
*
* @var int
*/
public $wcsg_sending_recipient_email;
/**
* Create an instance of the class.
*/
public function __construct() {
$this->id = 'recipient_completed_order';
$this->title = __( 'New Initial Order - Recipient', 'woocommerce-subscriptions' );
$this->description = __( 'This email is sent to recipients notifying them of subscriptions purchased for them.', 'woocommerce-subscriptions' );
$this->customer_email = true;
$this->heading = __( 'New Order', 'woocommerce-subscriptions' );
$this->subject = __( 'Your new subscriptions at {site_title}', 'woocommerce-subscriptions' );
$this->template_html = 'emails/recipient-new-initial-order.php';
$this->template_plain = 'emails/plain/recipient-new-initial-order.php';
$this->template_base = plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/gifting/';
// Trigger for this email.
add_action( 'wcsg_new_order_recipient_notification', array( $this, 'trigger' ), 10, 2 );
WC_Email::__construct();
}
/**
* Trigger function.
*
* @param int $recipient_user User ID.
* @param WP_Post[] $recipient_subscriptions Array of subscription post objects.
*/
public function trigger( $recipient_user, $recipient_subscriptions ) {
if ( $recipient_user ) {
$this->object = get_user_by( 'id', $recipient_user );
$this->recipient = stripslashes( $this->object->user_email );
$subscription = wcs_get_subscription( $recipient_subscriptions[0] );
$this->subscription_owner = WCS_Gifting::get_user_display_name( $subscription->get_user_id() );
$this->subscriptions = $recipient_subscriptions;
}
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->wcsg_sending_recipient_email = $recipient_user;
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
unset( $this->wcsg_sending_recipient_email );
}
/**
* Returns the content for the HTML version of the e-mail.
*/
public function get_content_html() {
ob_start();
wc_get_template(
$this->template_html,
array(
'email_heading' => $this->get_heading(),
'blogname' => $this->get_blogname(),
'recipient_user' => $this->object,
'subscription_purchaser' => $this->subscription_owner,
'subscriptions' => $this->subscriptions,
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
'additional_content' => $this->get_additional_content(),
),
'',
$this->template_base
);
return ob_get_clean();
}
/**
* Returns the content for the plain text version of the e-mail.
*/
public function get_content_plain() {
ob_start();
wc_get_template(
$this->template_plain,
array(
'email_heading' => $this->get_heading(),
'blogname' => $this->get_blogname(),
'recipient_user' => $this->object,
'subscription_purchaser' => $this->subscription_owner,
'subscriptions' => $this->subscriptions,
'sent_to_admin' => false,
'plain_text' => true,
'email' => $this,
'additional_content' => $this->get_additional_content(),
),
'',
$this->template_base
);
return ob_get_clean();
}
}

View File

@@ -0,0 +1,169 @@
<?php
/**
* Personal data erasers.
*
* @package WooCommerce Subscriptions Gifting\Privacy
* @version 2.0.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles erasing of Gifting information from WooCommerce.
*/
class WCSG_Privacy_Erasers {
/**
* Find and erase personal data from subscriptions linked to a user via recipient meta.
*
* Subscriptions are erased in blocks of 10 to avoid timeouts.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param string $email_address The user email address.
* @param int $page Page.
* @return array An array of response data to return to the WP eraser.
*/
public static function subscription_data_eraser( $email_address, $page ) {
$user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
$subscriptions = array();
if ( $user instanceof WP_User ) {
$subscriptions = WCSG_Recipient_Management::get_recipient_subscriptions(
$user->ID,
0,
array(
'posts_per_page' => 10,
'paged' => (int) $page,
)
);
$subscriptions = array_filter( array_map( 'wcs_get_subscription', $subscriptions ) );
}
return WCS_Privacy_Erasers::erase_subscription_data_and_generate_response( $subscriptions );
}
/**
* Find and erase personal data from subscription orders linked to a user via recipient meta.
*
* Orders are erased in blocks of 10 to avoid timeouts.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param string $email_address The user email address.
* @param int $page Page.
* @return array An array of response data to return to the WP eraser.
*/
public static function order_data_eraser( $email_address, $page ) {
$batch_size = 10;
$user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
$erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_order_data', 'no' ) );
$response = array(
'items_removed' => false,
'items_retained' => false,
'messages' => array(),
'done' => true,
);
if ( ! $user instanceof WP_User ) {
return $response;
}
$recipient_line_items = WCS_Gifting::get_recipient_order_items( $user->ID );
if ( empty( $recipient_line_items ) ) {
return $response;
}
$orders_ids = wp_list_pluck( $recipient_line_items, 'order_id', 'order_id' );
// Sort the orders by their ID so we can apply pagination between requests in a consistent way.
ksort( $orders_ids );
// Apply pagination.
$orders_ids = array_slice( $orders_ids, ( (int) $page - 1 ) * $batch_size, $batch_size );
if ( 0 < count( $orders_ids ) ) {
foreach ( $orders_ids as $order_id ) {
$order = wc_get_order( $order_id );
// We might have a subscription object here so make sure we have an order before continuing.
if ( ! wcs_is_order( $order ) ) {
continue;
}
if ( apply_filters( 'woocommerce_privacy_erase_order_personal_data', $erasure_enabled, $order ) ) {
// We can only anonymise all personal data from the order if it's a renewal, resubscribe or switch order. Parent orders contain purchaser details.
if ( wcs_order_contains_subscription( $order, array( 'renewal', 'switch', 'resubscribe' ) ) ) {
WC_Privacy_Erasers::remove_order_personal_data( $order );
/* Translators: %s Order number. */
$response['messages'][] = sprintf( __( 'Removed personal data from order %s.', 'woocommerce-subscriptions' ), $order->get_order_number() );
$response['items_removed'] = true;
} else {
self::remove_personal_recipient_line_item_data( $order, $user->ID );
/* Translators: %s Order number. */
$response['messages'][] = sprintf( __( 'Removed recipient personal data from order %s line items.', 'woocommerce-subscriptions' ), $order->get_order_number() );
$response['items_removed'] = true;
}
} else {
/* Translators: %s Order number. */
$response['messages'][] = sprintf( __( 'Personal data within order %s has been retained.', 'woocommerce-subscriptions' ), $order->get_order_number() );
$response['items_retained'] = true;
}
}
$response['done'] = $batch_size > count( $orders_ids );
}
return $response;
}
/**
* Remove personal recipient line item meta from an order or subscription.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param WC_Order|WC_Subscription $order The order or subscription object to remove recipient line item meta from.
* @param int $recipient_id Optional. Default behaviour is to remove all recipient line item meta. Pass a user ID to only remove line item meta specific to that user.
*/
public static function remove_personal_recipient_line_item_data( $order, $recipient_id = 0 ) {
if ( ! is_callable( array( $order, 'get_items' ) ) ) {
return;
}
foreach ( $order->get_items() as $line_item ) {
if ( ! $line_item->meta_exists( 'wcsg_recipient' ) ) {
continue;
}
if ( 0 !== $recipient_id ) {
// Get the user ID from the line item meta.
$user_id = str_replace( 'wcsg_recipient_id_', '', $line_item->get_meta( 'wcsg_recipient', true ) );
// Continue if we're not concerned with this user.
if ( ! is_numeric( $user_id ) || (int) $user_id !== $recipient_id ) {
continue;
}
}
$line_item->delete_meta_data( 'wcsg_recipient' );
$line_item->save();
}
}
/**
* Remove a recipient from a subscription.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param WC_Subscription $subscription The subscription object.
*/
public static function remove_recipient_meta( $subscription ) {
if ( is_callable( array( $subscription, 'delete_meta_data' ) ) ) {
$subscription->delete_meta_data( '_recipient_user' );
$subscription->save();
}
}
}

View File

@@ -0,0 +1,243 @@
<?php
/**
* Personal data exporters.
*
* @package WooCommerce Subscriptions Gifting\Privacy
* @version 2.0.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Gifting information exporter.
*/
class WCSG_Privacy_Exporters extends WCS_Privacy_Exporters {
/**
* Finds and exports subscription data linked to a user via recipient meta.
*
* Subscriptions are exported in blocks of 10 to avoid timeouts.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param string $email_address The user email address.
* @param int $page Page.
* @return array An array of personal data in name value pairs.
*/
public static function subscription_data_exporter( $email_address, $page ) {
$done = true;
$user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
$data_to_export = array();
// If we didn't get a user, exit early.
if ( ! $user instanceof WP_User ) {
return array(
'data' => $data_to_export,
'done' => $done,
);
}
$subscriptions = WCSG_Recipient_Management::get_recipient_subscriptions(
$user->ID,
0,
array(
'posts_per_page' => 10,
'paged' => (int) $page,
)
);
$subscriptions = array_filter( array_map( 'wcs_get_subscription', $subscriptions ) );
// Filter the properties exported so only the recipient's personal data stored on the subscription is exported.
add_filter( 'woocommerce_privacy_export_subscription_personal_data_props', array( __CLASS__, 'remove_purchaser_properties' ) );
if ( 0 < count( $subscriptions ) ) {
foreach ( $subscriptions as $subscription ) {
$data_to_export[] = array(
'group_id' => 'woocommerce_gifted_subscriptions',
'group_label' => __( 'Subscriptions Purchased for You', 'woocommerce-subscriptions' ),
'item_id' => 'subscription-' . $subscription->get_id(),
'data' => self::get_subscription_personal_data( $subscription ),
);
}
$done = 10 > count( $subscriptions );
}
remove_filter( 'woocommerce_privacy_export_subscription_personal_data_props', array( __CLASS__, 'remove_purchaser_properties' ) );
return array(
'data' => $data_to_export,
'done' => $done,
);
}
/**
* Remove the personal data properties which belong to the purchaser.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param array $exported_data_properties The subscription properties to export.
* @return array The recipient personal data properties to export.
*/
public static function remove_purchaser_properties( $exported_data_properties ) {
$purchaser_properties = array(
'total',
'customer_ip_address',
'customer_user_agent',
'formatted_billing_address',
'billing_phone',
'billing_email',
);
// Remove the properties which belong to the purchaser.
foreach ( $purchaser_properties as $property ) {
unset( $exported_data_properties[ $property ] );
}
return $exported_data_properties;
}
/**
* Finds and exports order data linked to a user via recipient line item meta.
*
* Orders are exported in blocks of 10 to avoid timeouts.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param string $email_address The user email address.
* @param int $page Page.
* @return array An array of personal data in name value pairs.
*/
public static function order_data_exporter( $email_address, $page ) {
$done = true;
$user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
$data_to_export = array();
$export_limit = 10;
// If we didn't find get a user, exit early.
if ( ! $user instanceof WP_User ) {
return array(
'data' => $data_to_export,
'done' => $done,
);
}
$recipients_line_items = WCS_Gifting::get_recipient_order_items( $user->ID );
if ( 0 < count( $recipients_line_items ) ) {
$recipient_order_items = array();
// Gather all the line items by their order ID so we have an array of line item IDs per order.
foreach ( $recipients_line_items as $item_data ) {
if ( isset( $item_data['order_id'], $item_data['order_item_id'] ) ) {
$recipient_order_items[ $item_data['order_id'] ][ $item_data['order_item_id'] ] = $item_data['order_item_id'];
}
}
// Sort the orders by their ID so we can apply pagination between requests in a consistent way.
ksort( $recipient_order_items );
// Apply a poor man's pagination.
$recipient_order_items = array_slice( $recipient_order_items, ( (int) $page - 1 ) * $export_limit, $export_limit, true );
foreach ( $recipient_order_items as $order_id => $line_item_ids ) {
$order = wc_get_order( $order_id );
if ( ! wcs_is_order( $order ) ) {
continue;
}
$order_items = $order->get_items();
// Check if we need to export shipping address details by checking if all line items belong to the recipient and if the order is a renewal, switch or resubscribe.
$export_shipping = ( array_keys( $order_items ) == array_keys( $line_item_ids ) ) && wcs_order_contains_subscription( $order, array( 'renewal', 'switch', 'resubscribe' ) ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
// Get the order items from the order which belong to this recipient.
$order_items = array_intersect_key( $order_items, $line_item_ids );
$data_to_export[] = array(
'group_id' => 'woocommerce_gifted_subscription_orders',
'group_label' => __( 'Orders Purchased for You', 'woocommerce-subscriptions' ),
'item_id' => 'order-' . $order_id,
'data' => self::get_recipient_order_personal_data( $order, $order_items, $export_shipping ),
);
}
$done = $export_limit > count( $recipient_order_items );
}
return array(
'data' => $data_to_export,
'done' => $done,
);
}
/**
* Get the recipient's personal data (key/value pairs) for an order object.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param WC_Order $order The order object.
* @param array $items The order line item objects which belong to the recipient in the order.
* @param bool $export_shipping Whether to export shipping address data.
* @return array The recipient's personal data.
*/
protected static function get_recipient_order_personal_data( $order, $items, $export_shipping ) {
$personal_data = array();
$props_to_export = apply_filters(
'woocommerce_privacy_export_recipient_order_personal_data_props',
array(
'order_number' => __( 'Order Number', 'woocommerce-subscriptions' ),
'date_created' => __( 'Order Date', 'woocommerce-subscriptions' ),
'items' => __( 'Items Purchased', 'woocommerce-subscriptions' ),
'formatted_shipping_address' => __( 'Shipping Address', 'woocommerce-subscriptions' ),
),
$order
);
if ( ! $export_shipping ) {
unset( $props_to_export['formatted_shipping_address'] );
}
foreach ( $props_to_export as $prop => $name ) {
$value = '';
switch ( $prop ) {
case 'items':
$item_names = array();
foreach ( $items as $item ) {
$item_names[] = $item->get_name() . ' x ' . $item->get_quantity();
}
$value = implode( ', ', $item_names );
break;
case 'date_created':
$value = wc_format_datetime( $order->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) );
break;
case 'formatted_shipping_address':
$value = preg_replace( '#<br\s*/?>#i', ', ', $order->{"get_$prop"}() );
break;
default:
if ( is_callable( array( $order, 'get_' . $prop ) ) ) {
$value = $order->{"get_$prop"}();
}
break;
}
$value = apply_filters( 'woocommerce_privacy_export_recipient_order_personal_data_prop', $value, $prop, $order );
if ( $value ) {
$personal_data[] = array(
'name' => $name,
'value' => $value,
);
}
}
/**
* Allow extensions to register their own personal data for this order for the export.
*
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.1.
* @param array $personal_data Array of name value pairs to expose in the export.
* @param WC_Order $order An order object.
*/
return apply_filters( 'woocommerce_privacy_export_recipient_order_personal_data', $personal_data, $order );
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Privacy/GDPR related functionality which ties into WordPress functionality.
*
* @package WooCommerce Subscriptions Gifting\Privacy
* @version 2.0.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Hooks into WooCommerce's privacy-related functionality.
*/
class WCSG_Privacy extends WC_Abstract_Privacy {
/**
* WCSG_Privacy constructor.
*/
public function __construct() {
parent::__construct( __( 'WooCommerce Subscriptions Gifting', 'woocommerce-subscriptions' ) );
// include our exporters and erasers.
include_once 'class-wcsg-privacy-exporters.php';
include_once 'class-wcsg-privacy-erasers.php';
$this->add_exporter( 'woocommerce-gifted-subscriptions-data', __( 'Recipient Subscription Data', 'woocommerce-subscriptions' ), array( 'WCSG_Privacy_Exporters', 'subscription_data_exporter' ) );
$this->add_exporter( 'woocommerce-gifted-order-data', __( 'Recipient Order Data', 'woocommerce-subscriptions' ), array( 'WCSG_Privacy_Exporters', 'order_data_exporter' ) );
$this->add_eraser( 'woocommerce-gifted-subscriptions-data', __( 'Recipient Subscription Data', 'woocommerce-subscriptions' ), array( 'WCSG_Privacy_Erasers', 'subscription_data_eraser' ) );
$this->add_eraser( 'woocommerce-gifted-order-data', __( 'Recipient Order Data', 'woocommerce-subscriptions' ), array( 'WCSG_Privacy_Erasers', 'order_data_eraser' ) );
}
/**
* Attach callbacks.
*/
protected function init() {
parent::init();
// When an order or subscription is anonymised, remove recipient line item meta.
add_action( 'woocommerce_privacy_before_remove_order_personal_data', array( 'WCSG_Privacy_Erasers', 'remove_personal_recipient_line_item_data' ) );
add_action( 'woocommerce_privacy_before_remove_subscription_personal_data', array( 'WCSG_Privacy_Erasers', 'remove_personal_recipient_line_item_data' ) );
// Remove recipient meta from a subscription when it is anonymised.
add_action( 'woocommerce_privacy_before_remove_subscription_personal_data', array( 'WCSG_Privacy_Erasers', 'remove_recipient_meta' ) );
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* WCS Gifting Compatibility functions.
*
* @package WooCommerce Subscriptions Gifting
*/
/**
* WooCommerce Compatibility functions
*
* Functions to take advantage of APIs added to new versions of WooCommerce while maintaining backward compatibility.
*
* @package WooCommerce Subscriptions Gifting/Functions
* @version 1.0.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Check if the installed version of WooCommerce is older than a specified version.
*
* @param string $version Version to check against.
* @return bool
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
*/
function wcsg_is_woocommerce_pre( $version ) {
if ( ! defined( 'WC_VERSION' ) || version_compare( WC_VERSION, $version, '<' ) ) {
$woocommerce_is_pre_version = true;
} else {
$woocommerce_is_pre_version = false;
}
return $woocommerce_is_pre_version;
}
/**
* Get an object's property value in a version compatible way.
*
* @param object $object Object.
* @param string $property Property name.
* @return mixed
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
*/
function wcsg_get_objects_property( $object, $property ) {
$value = '';
switch ( $property ) {
case 'order':
if ( is_callable( array( $object, 'get_parent' ) ) ) {
$value = $object->get_parent();
} else {
$value = $object->order;
}
break;
default:
$function = 'get_' . $property;
if ( is_callable( array( $object, $function ) ) ) {
$value = $object->$function();
} else {
$value = $object->$property;
}
break;
}
return $value;
}
/**
* Get an object's ID in a version compatible way.
*
* @param object $object Object.
* @return int
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 1.0.1.
*/
function wcsg_get_objects_id( $object ) {
if ( method_exists( $object, 'get_id' ) ) {
$id = $object->get_id();
} else {
$id = $object->id;
}
return $id;
}
/**
* Check if the active version of WooCommerce Subscriptions is older than the specified version.
*
* @param string $version Version to check against.
* @return bool
* @since 7.8.0 - Originally implemented in WooCommerce Subscriptions Gifting 2.0.0.
*/
function wcsg_is_wc_subscriptions_pre( $version ) {
if ( ! class_exists( 'WC_Subscriptions_Core_Plugin' ) && ! class_exists( 'WC_Subscriptions' ) ) {
_doing_it_wrong( __METHOD__, 'This method should not be called before plugins_loaded.', '2.0' );
return false;
}
$subscriptions_version = class_exists( 'WC_Subscriptions_Core_Plugin' ) ? WC_Subscriptions_Core_Plugin::instance()->get_plugin_version() : WC_Subscriptions::$version;
return version_compare( $subscriptions_version, $version, '<' );
}

View File

@@ -1259,8 +1259,15 @@ class WC_Subscriptions_Switcher {
* @since 2.0
*/
public static function cart_contains_switch_for_product( $product ) {
if ( ! is_object( $product ) ) {
$product = wc_get_product( $product );
}
$product_id = ( is_object( $product ) ) ? $product->get_id() : $product;
if ( ! $product ) {
return false;
}
$product_id = $product->get_id();
$switch_items = self::cart_contains_switches();
$switch_product_ids = array();

File diff suppressed because it is too large Load Diff

View File

@@ -50,3 +50,8 @@ parameters:
- '#Parameter .* has invalid type OrdersTableQuery.#'
- '#Constant WC_VERSION not found#'
- '#Method .* has invalid return type ActionScheduler_.*#'
- '#Function wc_memberships.* not found#'
- '#Parameter .* of method .* has invalid type WC_Memberships_Membership_Plan#'
- '#Access to an undefined property WCSG_Email_Completed_Renewal_Order::\$#'
- '#Access to an undefined property WC_Subscription::\$recipient_user#'
- '#Cannot access property \$order_date on bool.*#'

View File

@@ -96,3 +96,32 @@ if ( ! defined( 'ABSPATH' ) ) {
</span>
</p>
</div>
<?php
if ( WCSG_Admin::is_gifting_enabled() ) {
$variation_product_gifting = WC_Subscriptions_Product::get_gifting( $variation_product );
$is_following_gifting_global_setting = empty( $variation_product_gifting );
?>
<fieldset class="variable_subscription_gifting show_if_variable-subscription">
<p class="form-row form-field show_if_variable-subscription _subscription_gifting_field">
<label for="variable_subscription_gifting[<?php echo esc_attr( $loop ); ?>]">
<?php esc_html_e( 'Gifting', 'woocommerce-subscriptions' ); ?>
<?php
// @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wcs_help_tip( __( 'Allow shoppers to purchase a subscription as a gift.', 'woocommerce-subscriptions' ) );
?>
</label>
<select id="variable_subscription_gifting[<?php echo esc_attr( $loop ); ?>]" name="variable_subscription_gifting[<?php echo esc_attr( $loop ); ?>]" class="wc_input_subscription_gifting wc-enhanced-select">
<option value="" <?php selected( '', WC_Subscriptions_Product::get_gifting( $variation_product ) ); ?>>
<?php echo esc_html( WCSG_Admin::get_gifting_option_text() ); ?>
</option>
<option value="enabled" <?php selected( 'enabled', $variation_product_gifting ); ?>><?php esc_html_e( 'Enabled', 'woocommerce-subscriptions' ); ?></option>
<option value="disabled" <?php selected( 'disabled', $variation_product_gifting ); ?>><?php esc_html_e( 'Disabled', 'woocommerce-subscriptions' ); ?></option>
</select>
</p>
<?php
if ( ! $is_following_gifting_global_setting ) {
WCSG_Admin::get_gifting_global_override_text();
}
?>
</fieldset>
<?php } ?>

View File

@@ -0,0 +1,94 @@
<?php
/**
* Outputs the Status section for Subscriptions Gifting.
*
* @package WooCommerce Subscriptions Gifting/Templates/Admin
* @version 2.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! isset( $debug_data ) || ! is_array( $debug_data ) || empty( $debug_data ) ) {
return;
}
?>
<table class="wc_status_table widefat" cellspacing="0">
<thead>
<tr>
<th colspan="3" data-export-label="<?php echo esc_attr( $section_title ); ?>">
<h2><?php echo esc_html( $section_title ); ?>
<?php echo wc_help_tip( $section_tooltip ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
</h2></th>
</tr>
</thead>
<tbody>
<?php
foreach ( $debug_data as $section => $data ) :
// Use mark key if available, otherwise default back to the success key.
if ( isset( $data['mark'] ) ) {
$mark = $data['mark'];
} elseif ( isset( $data['success'] ) && $data['success'] ) {
$mark = 'yes';
} else {
$mark = 'error';
}
// Use mark_icon key if available, otherwise set based on $mark.
if ( isset( $data['mark_icon'] ) ) {
$mark_icon = $data['mark_icon'];
} elseif ( 'yes' === $mark ) {
$mark_icon = 'yes';
} else {
$mark_icon = 'no-alt';
}
?>
<tr>
<td data-export-label="<?php echo esc_attr( $data['label'] ); ?>"><?php echo esc_html( $data['name'] ); ?>:
</td>
<td class="help">&nbsp;</td>
<td>
<?php
if ( isset( $data['data'] ) ) {
if ( empty( $data['data'] ) ) {
echo '&ndash;';
continue;
}
$row_number = count( $data['data'] );
foreach ( $data['data'] as $row ) {
echo wp_kses_post( $row );
if ( 1 !== $row_number ) {
echo ', ';
}
echo '<br />';
$row_number--;
}
}
if ( isset( $data['note'] ) ) {
if ( empty( $mark ) ) {
echo wp_kses_post( $data['note'] );
} else {
?>
<mark class="<?php echo esc_html( $mark ); ?>">
<?php
if ( $mark_icon ) {
echo '<span class="dashicons dashicons-' . esc_attr( $mark_icon ) . '"></span> ';
}
echo wp_kses_post( $data['note'] );
?>
</mark>
<?php
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>

View File

@@ -0,0 +1,62 @@
<?php
/**
* Recipient customer new account email
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails
* @version 2.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<p><?php printf( esc_html__( 'Hi there,', 'woocommerce-subscriptions' ) ); ?></p>
<p>
<?php
// Translators: 1) is the purchaser's name, 2) is the blog's name.
printf( esc_html__( '%1$s just purchased a subscription for you at %2$s so we\'ve created an account for you to manage the subscription.', 'woocommerce-subscriptions' ), wp_kses( $subscription_purchaser, wp_kses_allowed_html( 'user_description' ) ), esc_html( $blogname ) );
?>
</p>
<p>
<?php
// Translators: placeholder is a username.
printf( esc_html__( 'Your username is: %s', 'woocommerce-subscriptions' ), '<strong>' . esc_html( $user_login ) . '</strong>' );
?>
</p>
<p><a class="link" href="<?php echo esc_url( add_query_arg( array( 'key' => $reset_key, 'id' => $user_id ), wc_get_endpoint_url( 'lost-password', '', wc_get_page_permalink( 'myaccount' ) ) ) ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound ?>">
<?php esc_html_e( 'Click here to set your password', 'woocommerce-subscriptions' ); ?></a></p>
<p>
<?php
printf(
/* Translators: placeholder is a link to "My Account" for setting up the recipient's details. */
esc_html__( 'To complete your account we just need you to fill in your shipping address here: %s.', 'woocommerce-subscriptions' ),
'<a href="' . esc_url( wc_get_endpoint_url( 'new-recipient-account', '', wc_get_page_permalink( 'myaccount' ) ) ) . '">' . esc_html__( 'My Account Details', 'woocommerce-subscriptions' ) . '</a>'
);
?>
</p>
<p>
<?php
printf(
/* Translators: placeholder is a link to "My Account". */
esc_html__( 'Once completed you may access your account area to view your subscription here: %s.', 'woocommerce-subscriptions' ),
'<a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '">' . esc_html__( 'My Account', 'woocommerce-subscriptions' ) . '</a>'
);
?>
</p>
<?php
/**
* Show user-defined additional content - this is set in each email's settings.
*/
if ( $additional_content ) {
echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) );
}
do_action( 'woocommerce_email_footer', $email ); ?>

View File

@@ -0,0 +1,40 @@
<?php
/**
* Recipient customer new account email
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails/Plain
* @version 2.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
echo '= ' . esc_html( wp_strip_all_tags( $email_heading ) ) . " =\n\n";
echo sprintf( esc_html__( 'Hi there,', 'woocommerce-subscriptions' ) ) . "\n\n";
// Translators: 1) is the purchaser's name, 2) is the blog's name.
echo sprintf( esc_html__( '%1$s just purchased a subscription for you at %2$s so we\'ve created an account for you to manage the subscription.', 'woocommerce-subscriptions' ), esc_html( $subscription_purchaser ), esc_html( $blogname ) ) . "\n\n";
// Translators: placeholder is a username.
echo sprintf( esc_html__( 'Your username is: %s', 'woocommerce-subscriptions' ), esc_html( $user_login ) ) . "\n";
// Translators: placeholder is the URL for resetting the password.
echo sprintf( esc_html__( 'Go here to set your password: %s', 'woocommerce-subscriptions' ), esc_url( add_query_arg( array( 'key' => $reset_key, 'id' => $user_id ), wc_get_endpoint_url( 'lost-password', '', wc_get_page_permalink( 'myaccount' ) ) ) ) ) . "\n\n"; // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
// Translators: placeholder is the URL for setting up the recipient's details.
echo sprintf( esc_html__( 'To complete your account we just need you to fill in your shipping address and you to change your password here: %s.', 'woocommerce-subscriptions' ), esc_url( wc_get_endpoint_url( 'new-recipient-account', '', wc_get_page_permalink( 'myaccount' ) ) ) ) . "\n\n";
// Translators: placeholder is the URL for "My Account".
echo sprintf( esc_html__( 'Once completed you may access your account area to view your subscription here: %s.', 'woocommerce-subscriptions' ), esc_url( wc_get_page_permalink( 'myaccount' ) ) ) . "\n\n";
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
/**
* Show user-defined additional content - this is set in each email's settings.
*/
if ( $additional_content ) {
echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) );
echo "\n\n----------------------------------------\n\n";
}
echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) );

View File

@@ -0,0 +1,33 @@
<?php
/**
* Recipient e-mail: completed renewal order.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails/Plain
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
echo esc_html( wp_strip_all_tags( $email_heading ) ) . "\n\n";
// translators: placeholder is the name of the site.
printf( esc_html__( 'Hi there. Your subscription renewal order with %s has been completed. Your order details are shown below for your reference:', 'woocommerce-subscriptions' ), esc_html( get_option( 'blogname' ) ) );
echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
if ( is_callable( array( 'WC_Subscriptions_Email', 'order_download_details' ) ) ) {
WC_Subscriptions_Email::order_download_details( $order, $sent_to_admin, $plain_text, $email );
}
do_action( 'wcs_gifting_email_order_details', $order, $sent_to_admin, $plain_text, $email );
echo "\n";
do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email );
do_action( 'woocommerce_subscriptions_gifting_recipient_email_details', $order, $sent_to_admin, $plain_text, $email );
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) );

View File

@@ -0,0 +1,15 @@
<?php
/**
* Recipient e-mail: address table.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails/Plain
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! wc_ship_to_billing_address_only() && $order->needs_shipping_address() && ( $shipping = $order->get_formatted_shipping_address() ) ) {
echo "\n" . esc_html( strtoupper( __( 'Shipping address', 'woocommerce-subscriptions' ) ) ) . "\n\n";
echo esc_html( preg_replace( '#<br\s*/?>#i', "\n", $shipping ) ) . "\n";
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Order details table shown in emails.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails/Plain
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
echo esc_html( $title ) . "\n\n";
echo wp_kses_post(
WCSG_Email::recipient_email_order_items_table(
$order,
array(
'show_sku' => $sent_to_admin,
'show_image' => '',
'image_size' => '',
'plain_text' => $plain_text,
'sent_to_admin' => $sent_to_admin,
)
)
);
echo "----------\n";

View File

@@ -0,0 +1,57 @@
<?php
/**
* Recipient e-mail: order items.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails/Plain
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$text_align = is_rtl() ? 'right' : 'left';
foreach ( $items as $item_id => $item ) {
$product = $item->get_product();
if ( apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
// Translators: placeholder is the product name.
echo sprintf( esc_html__( 'Product: %s', 'woocommerce-subscriptions' ), esc_html( $item->get_name() ) ) . "\n";
if ( $show_sku && is_object( $product ) && $product->get_sku() ) {
// Translators: placeholder is the product SKU.
echo sprintf( esc_html__( 'SKU: #%s', 'woocommerce-subscriptions' ), esc_html( $product->get_sku() ) ) . "\n";
}
// allow other plugins to add additional product information here.
do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $order, $plain_text );
echo esc_html(
wp_strip_all_tags(
wc_display_item_meta(
$item,
array(
'before' => "\n- ",
'separator' => "\n- ",
'after' => '',
'echo' => false,
'autop' => false,
)
)
)
);
// allow other plugins to add additional product information here.
do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $order, $plain_text );
// Translators: placeholder is the item's quantity.
echo "\n" . sprintf( esc_html__( 'Quantity: %s', 'woocommerce-subscriptions' ), esc_html( $item->get_quantity() ) ) . "\n";
}
if ( $show_purchase_note && is_object( $product ) && ( $purchase_note = $product->get_purchase_note() ) ) {
// Translators: placeholder is a purchase note.
echo sprintf( esc_html__( 'Purchase Note: %s', 'woocommerce-subscriptions' ), do_shortcode( $purchase_note ) ) . "\n\n";
}
}
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";

View File

@@ -0,0 +1,35 @@
<?php
/**
* Recipient e-mail: subscriptions table.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails/Plain
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
foreach ( $subscriptions as $subscription ) {
// Translators: placeholder is a subscription number.
echo sprintf( esc_html__( 'Subscription #%s', 'woocommerce-subscriptions' ), esc_html( $subscription->get_order_number() ) ) . "\n";
// Translators: placeholder is a date.
echo sprintf( esc_html__( 'Start Date: %s', 'woocommerce-subscriptions' ), esc_html( date_i18n( wc_date_format(), $subscription->get_time( 'date_created', 'site' ) ) ) ) . "\n";
// Translators: placeholder is a date.
echo sprintf( esc_html__( 'End Date: %s', 'woocommerce-subscriptions' ), ( 0 < $subscription->get_time( 'end' ) ) ? esc_html( date_i18n( wc_date_format(), $subscription->get_time( 'end', 'site' ) ) ) : esc_html_x( 'When Cancelled', 'Used as end date for an indefinite subscription', 'woocommerce-subscriptions' ) ) . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$subscription_details = array(
'recurring_amount' => '',
'subscription_period' => $subscription->get_billing_period(),
'subscription_interval' => $subscription->get_billing_interval(),
'initial_amount' => '',
'use_per_slash' => false,
);
$subscription_details = apply_filters( 'woocommerce_subscription_price_string_details', $subscription_details, $subscription );
// Translators: placeholder is a subscription's price string. For example, "$5 / month for 12 months".
echo sprintf( esc_html__( 'Period: %s', 'woocommerce-subscriptions' ), wp_kses_post( wcs_price_string( $subscription_details ) ) ) . "\n";
echo "----------\n";
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* Recipient customer new account email.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails/Plain
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
echo '= ' . esc_html( wp_strip_all_tags( $email_heading ) ) . " =\n\n";
echo sprintf( esc_html__( 'Hi there,', 'woocommerce-subscriptions' ) ) . "\n";
// translators: 1$: Purchaser's name and email, 2$ The name of the site.
echo sprintf( esc_html__( '%1$s just purchased %2$s for you at %3$s.', 'woocommerce-subscriptions' ), wp_kses( $subscription_purchaser, wp_kses_allowed_html( 'user_description' ) ), esc_html( _n( 'a subscription', 'subscriptions', count( $subscriptions ), 'woocommerce-subscriptions' ) ), esc_html( $blogname ) );
// translators: placeholder is the singular or plural form of "subscription".
echo sprintf( esc_html__( ' Details of the %s are shown below.', 'woocommerce-subscriptions' ), esc_html( _n( 'subscription', 'subscriptions', count( $subscriptions ), 'woocommerce-subscriptions' ) ) ) . "\n\n";
$new_recipient = get_user_meta( $recipient_user->ID, 'wcsg_update_account', true );
if ( 'true' == $new_recipient ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
echo esc_html__( 'We noticed you didn\'t have an account so we created one for you. Your account login details will have been sent to you in a separate email.', 'woocommerce-subscriptions' ) . "\n\n";
} else {
// translators: 1) is the singular or plural form of "subscription", 2) is a link to "My Account".
echo sprintf( esc_html__( 'You may access your account area to view your new %1$s here: %2$s.', 'woocommerce-subscriptions' ), esc_html( _n( 'subscription', 'subscriptions', count( $subscriptions ), 'woocommerce-subscriptions' ) ), esc_url( wc_get_page_permalink( 'myaccount' ) ) ) . "\n\n";
}
foreach ( $subscriptions as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
do_action( 'wcs_gifting_email_order_details', $subscription, $sent_to_admin, $plain_text, $email );
if ( is_callable( array( 'WC_Subscriptions_Email', 'order_download_details' ) ) ) {
WC_Subscriptions_Email::order_download_details( $subscription, $sent_to_admin, $plain_text, $email );
}
}
/**
* Show user-defined additional content - this is set in each email's settings.
*/
if ( $additional_content ) {
echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) );
echo "\n\n----------------------------------------\n\n";
}
echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) );

View File

@@ -0,0 +1,32 @@
<?php
/**
* Recipient e-mail: processing renewal order.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails/Plain
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
echo esc_html( wp_strip_all_tags( $email_heading ) ) . "\n\n";
echo esc_html__( 'Your subscription renewal order has been received and is now being processed. Your order details are shown below for your reference:', 'woocommerce-subscriptions' );
echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
if ( is_callable( array( 'WC_Subscriptions_Email', 'order_download_details' ) ) ) {
WC_Subscriptions_Email::order_download_details( $order, $sent_to_admin, $plain_text, $email );
}
do_action( 'wcs_gifting_email_order_details', $order, $sent_to_admin, $plain_text, $email );
echo "\n";
do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email );
do_action( 'woocommerce_subscriptions_gifting_recipient_email_details', $order, $sent_to_admin, $plain_text, $email );
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) );

View File

@@ -0,0 +1,34 @@
<?php
/**
* Recipient e-mail: completed renewal order.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<p>
<?php
// translators: placeholder is the name of the site.
printf( esc_html__( 'Hi there. Your subscription renewal order with %s has been completed. Your order details are shown below for your reference:', 'woocommerce-subscriptions' ), esc_html( get_option( 'blogname' ) ) );
?>
</p>
<?php
if ( is_callable( array( 'WC_Subscriptions_Email', 'order_download_details' ) ) ) {
WC_Subscriptions_Email::order_download_details( $order, $sent_to_admin, $plain_text, $email );
}
?>
<?php do_action( 'wcs_gifting_email_order_details', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php do_action( 'woocommerce_subscriptions_gifting_recipient_email_details', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php do_action( 'woocommerce_email_footer', $email ); ?>

View File

@@ -0,0 +1,22 @@
<?php
/**
* Recipient email address table.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<table id="addresses" cellspacing="0" cellpadding="0" style="width: 100%; vertical-align: top; margin-bottom: 40px; padding:0;" border="0">
<tr>
<?php if ( ! wc_ship_to_billing_address_only() && $order->needs_shipping_address() && ( $shipping = $order->get_formatted_shipping_address() ) ) : ?>
<td style="font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif; padding:0;" valign="top" width="50%">
<h2><?php echo esc_html__( 'Shipping address', 'woocommerce-subscriptions' ); ?></h2>
<address class="address"><?php echo wp_kses_post( $shipping ); ?></address>
</td>
<?php endif; ?>
</tr>
</table>

View File

@@ -0,0 +1,36 @@
<?php
/**
* Recipient e-mail: order details.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<h2><?php echo esc_html( $title ); ?></h2>
<table cellspacing="0" cellpadding="6" style="margin: 0 0 18px; width: 100%; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;">
<thead>
<tr>
<th class="td" scope="col" style="text-align:left;"><?php esc_html_e( 'Product', 'woocommerce-subscriptions' ); ?></th>
<th class="td" scope="col" style="text-align:left;"><?php esc_html_e( 'Quantity', 'woocommerce-subscriptions' ); ?></th>
</tr>
</thead>
<tbody>
<?php
echo wp_kses_post(
WCSG_Email::recipient_email_order_items_table(
$order,
array(
'show_sku' => $sent_to_admin,
'show_image' => '',
'image_size' => '',
'plain_text' => $plain_text,
'sent_to_admin' => $sent_to_admin,
)
)
);
?>
</tbody>
</table>

View File

@@ -0,0 +1,58 @@
<?php
/**
* Recipient e-mail - order items.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$text_align = is_rtl() ? 'right' : 'left';
foreach ( $items as $item_id => $item ) :
$product = $item->get_product();
if ( apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
?>
<tr class="<?php echo esc_attr( apply_filters( 'woocommerce_order_item_class', 'order_item', $item, $order ) ); ?>">
<td class="td" style="text-align: <?php echo wp_kses_post( $text_align ); ?>; vertical-align:middle; border: 1px solid #eee; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif; word-wrap:break-word;">
<?php
// Show title/image etc.
if ( $show_image ) {
echo wp_kses_post( apply_filters( 'woocommerce_order_item_thumbnail', '<div style="margin-bottom: 5px"><img src="' . ( $product->get_image_id() ? current( wp_get_attachment_image_src( $product->get_image_id(), 'thumbnail' ) ) : wc_placeholder_img_src() ) . '" alt="' . esc_attr__( 'Product image', 'woocommerce-subscriptions' ) . '" height="' . esc_attr( $image_size[1] ) . '" width="' . esc_attr( $image_size[0] ) . '" style="vertical-align:middle; margin-' . ( is_rtl() ? 'left' : 'right' ) . ': 10px;" /></div>', $item ) );
}
// Product name.
echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, false ) );
// SKU.
if ( $show_sku && is_object( $product ) && $product->get_sku() ) {
// Translators: placeholder is a product SKU.
sprintf( __( ' (#%s)', 'woocommerce-subscriptions' ), $product->get_sku() );
}
// allow other plugins to add additional product information here.
do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $order, $plain_text );
wc_display_item_meta( $item );
// allow other plugins to add additional product information here.
do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $order, $plain_text );
?>
</td>
<td class="td" style="text-align:<?php echo wp_kses_post( $text_align ); ?>; vertical-align:middle; border: 1px solid #eee; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;"><?php echo wp_kses_post( apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ) ); ?></td>
</tr>
<?php
}
if ( $show_purchase_note && is_object( $product ) && ( $purchase_note = $product->get_purchase_note() ) ) :
?>
<tr>
<td colspan="3" style="text-align:<?php echo wp_kses_post( $text_align ); ?>; vertical-align:middle; border: 1px solid #eee; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;"><?php echo wp_kses_post( wpautop( do_shortcode( $purchase_note ) ) ); ?></td>
</tr>
<?php endif; ?>
<?php endforeach; ?>

View File

@@ -0,0 +1,44 @@
<?php
/**
* Recipient e-mail: subscriptions table.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<h2><?php esc_html_e( 'Subscription Information', 'woocommerce-subscriptions' ); ?></h2>
<table cellspacing="0" cellpadding="6" style="margin: 0 0 18px; width: 100%; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;">
<thead>
<tr>
<th class="td" scope="col" style="text-align:left;"><?php esc_html_e( 'Subscription', 'woocommerce-subscriptions' ); ?></th>
<th class="td" scope="col" style="text-align:left;"><?php echo esc_html_x( 'Start Date', 'table heading', 'woocommerce-subscriptions' ); ?></th>
<th class="td" scope="col" style="text-align:left;"><?php echo esc_html_x( 'End Date', 'table heading', 'woocommerce-subscriptions' ); ?></th>
<th class="td" scope="col" style="text-align:left;"><?php echo esc_html_x( 'Period', 'table heading', 'woocommerce-subscriptions' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $subscriptions as $subscription ) : ?>
<tr>
<td class="td" scope="row" style="text-align:left;"><a href="<?php echo esc_url( $subscription->get_view_order_url() ); ?>"><?php /* Translators: placeholder is a subscription number. */ echo sprintf( esc_html_x( '#%s', 'subscription number in email table. (eg: #106)', 'woocommerce-subscriptions' ), esc_html( $subscription->get_order_number() ) ); ?></a></td>
<td class="td" scope="row" style="text-align:left;"><?php echo esc_html( date_i18n( wc_date_format(), $subscription->get_time( 'date_created', 'site' ) ) ); ?></td>
<td class="td" scope="row" style="text-align:left;"><?php echo esc_html( ( 0 < $subscription->get_time( 'end' ) ) ? date_i18n( wc_date_format(), $subscription->get_time( 'end', 'site' ) ) : _x( 'When Cancelled', 'Used as end date for an indefinite subscription', 'woocommerce-subscriptions' ) ); ?></td>
<td class="td" scope="row" style="text-align:left;">
<?php
$subscription_details = array(
'recurring_amount' => '',
'subscription_period' => $subscription->get_billing_period(),
'subscription_interval' => $subscription->get_billing_interval(),
'initial_amount' => '',
'use_per_slash' => false,
);
$subscription_details = apply_filters( 'woocommerce_subscription_price_string_details', $subscription_details, $subscription );
echo wp_kses_post( wcs_price_string( $subscription_details ) );
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>

View File

@@ -0,0 +1,69 @@
<?php
/**
* Recipient new subscription(s) notification email.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<p><?php printf( esc_html__( 'Hi there,', 'woocommerce-subscriptions' ) ); ?></p>
<p>
<?php
// Translators: 1) is the subscription's purchaser, 2) is either the singular or plural form of "subscription" and 3) is the blog's name.
printf( esc_html__( '%1$s just purchased %2$s for you at %3$s.', 'woocommerce-subscriptions' ), wp_kses( $subscription_purchaser, wp_kses_allowed_html( 'user_description' ) ), esc_html( _n( 'a subscription', 'subscriptions', count( $subscriptions ), 'woocommerce-subscriptions' ) ), esc_html( $blogname ) );
?>
<?php
// Translators: placeholder is either the singular or plural form of "subscription".
printf( esc_html__( ' Details of the %s are shown below.', 'woocommerce-subscriptions' ), esc_html( _n( 'subscription', 'subscriptions', count( $subscriptions ), 'woocommerce-subscriptions' ) ) );
?>
</p>
<?php
$new_recipient = get_user_meta( $recipient_user->ID, 'wcsg_update_account', true );
if ( 'true' == $new_recipient ) : // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
?>
<p><?php esc_html_e( 'We noticed you didn\'t have an account so we created one for you. Your account login details will have been sent to you in a separate email.', 'woocommerce-subscriptions' ); ?></p>
<?php else : ?>
<p>
<?php
printf(
/* Translators: 1) is either the singular or plural form of "subscription", 2) is an <a> tag pointing to "My Account", 3) is the closing </a> tag. */
esc_html__( 'You may access your account area to view your new %1$s here: %2$sMy Account%3$s.', 'woocommerce-subscriptions' ),
esc_html( _n( 'subscription', 'subscriptions', count( $subscriptions ), 'woocommerce-subscriptions' ) ),
'<a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '">',
'</a>'
);
?>
</p>
<?php
endif;
foreach ( $subscriptions as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
do_action( 'wcs_gifting_email_order_details', $subscription, $sent_to_admin, $plain_text, $email );
if ( is_callable( array( 'WC_Subscriptions_Email', 'order_download_details' ) ) ) {
WC_Subscriptions_Email::order_download_details( $subscription, $sent_to_admin, $plain_text, $email );
}
}
/**
* Show user-defined additional content - this is set in each email's settings.
*/
if ( $additional_content ) {
echo wp_kses_post( wpautop( wptexturize( $additional_content ) ) );
}
do_action( 'woocommerce_email_footer', $email );

View File

@@ -0,0 +1,29 @@
<?php
/**
* Recipient e-mail: processing renewal order.
*
* @package WooCommerce Subscriptions Gifting/Templates/Emails
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<p><?php esc_html_e( 'Your subscription renewal order has been received and is now being processed. Your order details are shown below for your reference:', 'woocommerce-subscriptions' ); ?></p>
<?php
if ( is_callable( array( 'WC_Subscriptions_Email', 'order_download_details' ) ) ) {
WC_Subscriptions_Email::order_download_details( $order, $sent_to_admin, $plain_text, $email );
}
?>
<?php do_action( 'wcs_gifting_email_order_details', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php do_action( 'woocommerce_subscriptions_gifting_recipient_email_details', $order, $sent_to_admin, $plain_text, $email ); ?>
<?php do_action( 'woocommerce_email_footer', $email ); ?>

View File

@@ -0,0 +1,48 @@
<?php
/**
* Add recipient details
*
* @package WooCommerce Subscriptions Gifting/Templates
* @version 2.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<div class="wcsg_add_recipient_fields_container">
<input type="checkbox" id="gifting_<?php echo esc_attr( $id ); ?>_option" class="woocommerce_subscription_gifting_checkbox <?php echo esc_attr( implode( ' ', $checkbox_field_args['class'] ) ); ?>" style="<?php echo esc_attr( implode( '; ', $checkbox_field_args['style_attributes'] ) ); ?>" value="gift" <?php checked( $checkbox_field_args['checked'] ); ?> <?php disabled( $checkbox_field_args['disabled'] ); ?> />
<label for="gifting_<?php echo esc_attr( $id ); ?>_option">
<?php echo esc_html( apply_filters( 'wcsg_enable_gifting_checkbox_label', get_option( WCSG_Admin::$option_prefix . '_gifting_checkbox_text', __( 'This is a gift', 'woocommerce-subscriptions' ) ) ) ); ?>
</label>
<div class="wcsg_add_recipient_fields <?php echo esc_attr( implode( ' ', $container_css_class ) ); ?>" style="<?php echo esc_attr( implode( ' ', $container_style_attributes ) ); ?>">
<?php echo $nonce_field; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<p class="form-row form-row <?php echo esc_attr( implode( ' ', $email_field_args['class'] ) ); ?>" style="<?php echo esc_attr( implode( '; ', $email_field_args['style_attributes'] ) ); ?>">
<input
aria-label="<?php echo esc_attr( __(
'Gifting recipient',
'woocommerce-subscriptions'
) ); ?>"
data-recipient="<?php echo esc_attr( $email ); ?>"
type="email"
class="input-text recipient_email"
name="recipient_email[<?php echo esc_attr( $id ); ?>]" id="recipient_email[<?php echo esc_attr( $id ); ?>]"
placeholder="<?php echo esc_attr( $email_field_args['placeholder'] ); ?>"
value="<?php echo esc_attr( $email );
?>
"/>
</p>
<?php do_action( 'wcsg_add_recipient_fields' ); ?>
<div class="wc-shortcode-components-validation-error" role="alert">
<p id="shortcode-validate-error-invalid-gifting-recipient">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" width="24" height="24" aria-hidden="true" focusable="false">
<path d="M10 2c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8zm1.13 9.38l.35-6.46H8.52l.35 6.46h2.26zm-.09 3.36c.24-.23.37-.55.37-.96 0-.42-.12-.74-.36-.97s-.59-.35-1.06-.35-.82.12-1.07.35-.37.55-.37.97c0 .41.13.73.38.96.26.23.61.34 1.06.34s.8-.11 1.05-.34z">
</path>
</svg>
<span><?php esc_html_e( 'Please enter a valid email address', 'woocommerce-subscriptions' ); ?></span>
</p>
</div>
</div>
</div>

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