Compare commits
10 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
56e03c0544 | |
|
|
bfe7fb609d | |
|
|
c7e30d354e | |
|
|
dda5b19e32 | |
|
|
c8ec766bf2 | |
|
|
7b355505bf | |
|
|
708a1fa4a4 | |
|
|
7d081fb691 | |
|
|
9932a571ff | |
|
|
7041a483f0 |
|
|
@ -77,6 +77,14 @@ a.close-subscriptions-search {
|
|||
display: block;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.woocommerce_options_panel ._subscription_downloads_field .description,
|
||||
.woocommerce_options_panel .subscription_linked_downloadable_products .description {
|
||||
clear: both;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Variation Subscription Product Sync Settings */
|
||||
.variable_subscription_sync .subscription_sync_annual > .select2-container {
|
||||
max-width: 48%;
|
||||
|
|
@ -85,7 +93,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 +107,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 +141,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 {
|
||||
|
|
@ -356,9 +393,16 @@ a.close-subscriptions-search {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#woocommerce-product-data
|
||||
.form-field._subscription_downloads_field
|
||||
.select2-selection__choice {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Variation Pricing Fields with WooCommerce 3.0+ */
|
||||
.wc-metaboxes-wrapper .variable_subscription_trial label,
|
||||
.wc-metaboxes-wrapper .variable_subscription_pricing label,
|
||||
.wc-metaboxes-wrapper .variable_subscription_gifting label,
|
||||
.wc-metaboxes-wrapper .variable_subscription_sync label {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -947,3 +991,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
.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);
|
||||
}
|
||||
|
||||
.wcsg_add_recipient_fields_container label {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.woocommerce-cart-form__cart-item .wcsg_add_recipient_fields_container label {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wcsg_add_recipient_fields_container .wcsg_add_recipient_fields .woocommerce_subscriptions_gifting_recipient_email {
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.wcsg_add_recipient_fields_container .wcsg_add_recipient_fields.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wcsg_add_recipient_fields_container .recipient_email:focus {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
#woocommerce_subscriptions_gifting_field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#woocommerce_subscriptions_gifting_field div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ body.wcs-modal-open {
|
|||
}
|
||||
|
||||
.wcs-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 0;
|
||||
|
|
@ -17,6 +17,7 @@ body.wcs-modal-open {
|
|||
}
|
||||
|
||||
.wcs-modal.open {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
|
@ -50,6 +51,10 @@ body.wcs-modal-open {
|
|||
top: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wcs-modal .content-wrapper .modal-header {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,17 @@
|
|||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
button.subscription-auto-renew-toggle,
|
||||
button.subscription-auto-renew-toggle:hover {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.subscription-auto-renew-toggle {
|
||||
margin-left: 5px;
|
||||
margin-bottom: 2px;
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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;
|
||||
|
||||
|
|
@ -618,6 +639,53 @@ jQuery( function ( $ ) {
|
|||
},
|
||||
} );
|
||||
|
||||
/**
|
||||
* Relocates the linked downloadable product fields in the context of a variable subscription product, and
|
||||
* selectively shows/hides it, depending on whether the variation is downloadable or not.
|
||||
*/
|
||||
function initLinkedDownloadableProductFieldsForVariationProduct() {
|
||||
$( '#variable_product_options .variable_subscription_linked_downloadable_products' )
|
||||
.not( '.wcs_moved' )
|
||||
.each( function () {
|
||||
const $this = $( this );
|
||||
const $variablePricing = $( this ).siblings( '.variable_pricing' );
|
||||
|
||||
const $downloadableFiles = $( this ).siblings( '.form-row.downloadable_files' ).parent( 'div' );
|
||||
|
||||
$this.insertAfter( $downloadableFiles );
|
||||
$this.addClass( 'wcs_gifting_moved' );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the linked downloadable product fields to the correct location (which is underneath the downloadable files
|
||||
* section).
|
||||
*/
|
||||
function relocateLinkedDownloadableProductFields() {
|
||||
const $linkedDownloadableProducts = $( '.subscription_linked_downloadable_products_section' );
|
||||
const $productTypeSelector = $('select#product-type');
|
||||
|
||||
const showHide = () => {
|
||||
$productTypeSelector.val() === WCSubscriptions.productType
|
||||
? $linkedDownloadableProducts.show()
|
||||
: $linkedDownloadableProducts.hide();
|
||||
};
|
||||
|
||||
$linkedDownloadableProducts
|
||||
.not( '.wcs_moved' )
|
||||
.each( function () {
|
||||
const $downloadableFilesSection = $( '.form-field.downloadable_files' ).parent( 'div' );
|
||||
|
||||
if ( $downloadableFilesSection.length === 1 ) {
|
||||
$( this ).insertAfter( $downloadableFilesSection );
|
||||
$( this ).addClass( 'wcs_moved' );
|
||||
}
|
||||
} );
|
||||
|
||||
showHide();
|
||||
$productTypeSelector.on( 'change', showHide );
|
||||
}
|
||||
|
||||
$( '.options_group.pricing ._sale_price_field .description' ).prepend(
|
||||
'<span id="sale-price-period" style="display: none;"></span>'
|
||||
);
|
||||
|
|
@ -632,21 +700,32 @@ 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, gifting and linked downloadable product sections to a better location in
|
||||
// the DOM on load. We do this because these sections are initially loaded at the end of the variable product
|
||||
// options section, which is not the best location for them, so we move to before the "Sale price" section.
|
||||
if (
|
||||
$( '#variable_product_options .variable_subscription_pricing' ).length >
|
||||
0
|
||||
) {
|
||||
$.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();
|
||||
initLinkedDownloadableProductFieldsForVariationProduct();
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -660,6 +739,7 @@ jQuery( function ( $ ) {
|
|||
$.showHideSyncOptions();
|
||||
$.disableEnableOneTimeShipping();
|
||||
$.showHideSubscriptionsPanels();
|
||||
relocateLinkedDownloadableProductFields();
|
||||
}
|
||||
|
||||
// Update subscription ranges when subscription period or interval is changed
|
||||
|
|
@ -1030,6 +1110,41 @@ jQuery( function ( $ ) {
|
|||
} );
|
||||
}
|
||||
|
||||
var $giftingEnableCheckbox = $(
|
||||
document.getElementById(
|
||||
'woocommerce_subscriptions_gifting_enable_gifting'
|
||||
)
|
||||
),
|
||||
$giftingRadios = $( '.wc-settings-row-gifting-radios' ),
|
||||
$giftingCheckboxText = $( '.wc-settings-row-gifting-checkbox-text' ),
|
||||
$giftingDownloadableProducts = $(
|
||||
'.wc-settings-row-gifting-downloadable-products'
|
||||
);
|
||||
|
||||
if ( $giftingEnableCheckbox.length > 0 ) {
|
||||
function toggleGiftingCheckbox( checked ) {
|
||||
if ( checked ) {
|
||||
$giftingRadios.show();
|
||||
$giftingCheckboxText.show();
|
||||
$giftingDownloadableProducts.show();
|
||||
$giftingEnableCheckbox.closest( 'tr' ).addClass( 'checked' );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$giftingRadios.hide();
|
||||
$giftingCheckboxText.hide();
|
||||
$giftingDownloadableProducts.hide();
|
||||
$giftingEnableCheckbox.closest( 'tr' ).removeClass( 'checked' );
|
||||
}
|
||||
|
||||
$giftingEnableCheckbox.on( 'change', function () {
|
||||
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
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ jQuery( function ( $ ) {
|
|||
var $early_renewal_modal_content = $( '.wcs-modal > .content-wrapper' );
|
||||
|
||||
function getTxtColor() {
|
||||
if ( ! txtColor && $icon && $icon.length ) {
|
||||
txtColor = getComputedStyle( $icon[ 0 ] ).color;
|
||||
if ( ! txtColor ) {
|
||||
// Create a temporary link to get the theme's accent color.
|
||||
var $tempLink = $( '<a href="#" style="display:none;"></a>' ).appendTo( $toggleContainer );
|
||||
txtColor = getComputedStyle( $tempLink[ 0 ] ).color;
|
||||
$tempLink.remove();
|
||||
}
|
||||
|
||||
return txtColor;
|
||||
|
|
@ -38,9 +41,6 @@ jQuery( function ( $ ) {
|
|||
function onToggle( e ) {
|
||||
e.preventDefault();
|
||||
|
||||
// Remove focus from the toggle element.
|
||||
$toggle.trigger( 'blur' );
|
||||
|
||||
// Ignore the request if the toggle is disabled.
|
||||
if ( $toggle.hasClass( 'subscription-auto-renew-toggle--disabled' ) ) {
|
||||
return;
|
||||
|
|
@ -105,17 +105,20 @@ jQuery( function ( $ ) {
|
|||
$icon.removeClass( 'fa-toggle-off' ).addClass( 'fa-toggle-on' );
|
||||
$toggle
|
||||
.removeClass( 'subscription-auto-renew-toggle--off' )
|
||||
.addClass( 'subscription-auto-renew-toggle--on' );
|
||||
.addClass( 'subscription-auto-renew-toggle--on' )
|
||||
.attr( 'aria-checked', 'true' );
|
||||
}
|
||||
|
||||
function displayToggleOff() {
|
||||
$icon.removeClass( 'fa-toggle-on' ).addClass( 'fa-toggle-off' );
|
||||
$toggle
|
||||
.removeClass( 'subscription-auto-renew-toggle--on' )
|
||||
.addClass( 'subscription-auto-renew-toggle--off' );
|
||||
.addClass( 'subscription-auto-renew-toggle--off' )
|
||||
.attr( 'aria-checked', 'false' );
|
||||
}
|
||||
|
||||
function blockToggle() {
|
||||
$toggle.addClass( 'subscription-auto-renew-toggle--disabled' );
|
||||
$toggleContainer.block( {
|
||||
message: null,
|
||||
overlayCSS: { opacity: 0.0 },
|
||||
|
|
@ -123,6 +126,7 @@ jQuery( function ( $ ) {
|
|||
}
|
||||
|
||||
function unblockToggle() {
|
||||
$toggle.removeClass( 'subscription-auto-renew-toggle--disabled' );
|
||||
$toggleContainer.unblock();
|
||||
}
|
||||
|
||||
|
|
@ -165,5 +169,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
|
||||
);
|
||||
} );
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,358 @@
|
|||
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' );
|
||||
}
|
||||
} )
|
||||
.removeClass( 'hidden' );
|
||||
|
||||
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 )
|
||||
.addClass( 'hidden' );
|
||||
|
||||
const recipientEmailElement = $( this )
|
||||
.closest( '.wcsg_add_recipient_fields_container' )
|
||||
.find( '.recipient_email' );
|
||||
recipientEmailElement.val( '' );
|
||||
hideValidationErrorForEmailField( recipientEmailElement );
|
||||
setShippingAddressNoticeVisibility( true );
|
||||
|
||||
if ( $( 'form.checkout' ).length !== 0 ) {
|
||||
// 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 && ! allValid ) {
|
||||
$updateCartButton.prop( 'disabled', true );
|
||||
}
|
||||
|
||||
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 );
|
||||
} );
|
||||
} );
|
||||
|
|
@ -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();
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
jQuery( function ( $ ) {
|
||||
const modals = $( '.wcs-modal' );
|
||||
const $modals = $( '.wcs-modal' );
|
||||
let $currentModal;
|
||||
let $triggerElement;
|
||||
|
||||
// Resize all open modals on window resize.
|
||||
$( window ).on( 'resize', resizeModals );
|
||||
|
||||
// Initialize modals
|
||||
$( modals ).each( function () {
|
||||
$( $modals ).each( function () {
|
||||
trigger = $( this ).data( 'modal-trigger' );
|
||||
$( trigger ).on( 'click', { modal: this }, show_modal );
|
||||
} );
|
||||
|
|
@ -18,34 +20,35 @@ jQuery( function ( $ ) {
|
|||
* @param {JQuery event} event
|
||||
*/
|
||||
function show_modal( event ) {
|
||||
const modal = $( event.data.modal );
|
||||
$triggerElement = $( event.target );
|
||||
$currentModal = $( event.data.modal );
|
||||
|
||||
if ( ! should_show_modal( modal ) ) {
|
||||
if ( ! should_show_modal( $currentModal ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent the trigger element event being triggered.
|
||||
event.preventDefault();
|
||||
|
||||
const contentWrapper = modal.find( '.content-wrapper' );
|
||||
const close = modal.find( '.close' );
|
||||
const $contentWrapper = $currentModal.find( '.content-wrapper' );
|
||||
const $close = $currentModal.find( '.close' );
|
||||
|
||||
modal.trigger( 'focus' );
|
||||
modal.addClass( 'open' );
|
||||
|
||||
resizeModal( modal );
|
||||
$currentModal.addClass( 'open' );
|
||||
resizeModal( $currentModal );
|
||||
|
||||
$( document.body ).toggleClass( 'wcs-modal-open', true );
|
||||
$currentModal.focus();
|
||||
document.addEventListener( 'focusin', keepFocusInModal );
|
||||
|
||||
// Attach callbacks to handle closing the modal.
|
||||
close.on( 'click', () => close_modal( modal ) );
|
||||
modal.on( 'click', () => close_modal( modal ) );
|
||||
contentWrapper.on( 'click', ( e ) => e.stopPropagation() );
|
||||
$close.on( 'click', () => close_modal( $currentModal ) );
|
||||
$currentModal.on( 'click', () => close_modal( $currentModal ) );
|
||||
$contentWrapper.on( 'click', ( e ) => e.stopPropagation() );
|
||||
|
||||
// Close the modal if the escape key is pressed.
|
||||
modal.on( 'keyup', function ( e ) {
|
||||
$currentModal.on( 'keyup', function ( e ) {
|
||||
if ( 27 === e.keyCode ) {
|
||||
close_modal( modal );
|
||||
close_modal( $currentModal );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
|
@ -53,14 +56,17 @@ jQuery( function ( $ ) {
|
|||
/**
|
||||
* Closes a modal and resets any forced height styles.
|
||||
*
|
||||
* @param {JQuery Object} modal
|
||||
* @param {JQuery Object} $modal
|
||||
*/
|
||||
function close_modal( modal ) {
|
||||
modal.removeClass( 'open' );
|
||||
$( modal ).find( '.content-wrapper' ).css( 'height', '' );
|
||||
function close_modal( $modal ) {
|
||||
$modal.removeClass( 'open' );
|
||||
$( $modal ).find( '.content-wrapper' ).css( 'height', '' );
|
||||
|
||||
if ( 0 === modals.filter( '.open' ).length ) {
|
||||
if ( 0 === $modals.filter( '.open' ).length ) {
|
||||
$( document.body ).removeClass( 'wcs-modal-open' );
|
||||
$currentModal = false;
|
||||
document.removeEventListener( 'focusin', keepFocusInModal );
|
||||
$triggerElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +92,7 @@ jQuery( function ( $ ) {
|
|||
* Resize all open modals to fit the display.
|
||||
*/
|
||||
function resizeModals() {
|
||||
$( modals ).each( function () {
|
||||
$( $modals ).each( function () {
|
||||
if ( ! $( this ).hasClass( 'open' ) ) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -98,17 +104,28 @@ jQuery( function ( $ ) {
|
|||
/**
|
||||
* Resize a modal to fit the display.
|
||||
*
|
||||
* @param {JQuery Object} modal
|
||||
* @param {JQuery Object} $modal
|
||||
*/
|
||||
function resizeModal( modal ) {
|
||||
var modal_container = $( modal ).find( '.content-wrapper' );
|
||||
function resizeModal( $modal ) {
|
||||
const $modal_container = $( $modal ).find( '.content-wrapper' );
|
||||
|
||||
// On smaller displays the height is already forced to be 100% in CSS. We just clear any height we might set previously.
|
||||
if ( $( window ).width() <= 414 ) {
|
||||
modal_container.css( 'height', '' );
|
||||
} else if ( modal_container.height() > $( window ).height() ) {
|
||||
$modal_container.css( 'height', '' );
|
||||
} else if ( $modal_container.height() > $( window ).height() ) {
|
||||
// Force the container height to trigger scroll etc if it doesn't fit on the screen.
|
||||
modal_container.css( 'height', '90%' );
|
||||
$modal_container.css( 'height', '90%' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If focus moves out of the open modal, return focus to it.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
function keepFocusInModal( event ) {
|
||||
if ( $currentModal && ! $currentModal[0].contains( event.target ) ) {
|
||||
$currentModal.focus();
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
|
|
|||
|
|
@ -1,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;
|
||||
},
|
||||
} );
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
@ -1 +0,0 @@
|
|||
// Add styles here.
|
||||
|
|
@ -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 }
|
||||
/>
|
||||
) );
|
||||
};
|
||||
|
|
@ -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 }
|
||||
/>
|
||||
) );
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-i18n', 'wp-primitives'), 'version' => '8bad0ce18409676b15d1');
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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}
|
||||
.wcs-recurring-totals-panel{padding:1em 0 0;position:relative}.wcs-recurring-totals-panel:after{border-style:solid;border-width:1px 0;bottom:0;content:"";display:block;right:0;opacity:.3;pointer-events:none;position:absolute;left:0;top:0}.wcs-recurring-totals-panel+.wcs-recurring-totals-panel:after{border-top-width:0}.wcs-recurring-totals-panel .wc-block-components-panel .wc-block-components-totals-item{padding-right:0;padding-left:0}.wcs-recurring-totals-panel .wc-block-components-totals-item__label:first-letter{text-transform:capitalize}.wcs-recurring-totals-panel .wcs-recurring-totals-panel__title .wc-block-components-totals-item__label{font-weight:500}.wcs-recurring-totals-panel__title{margin:0}.wc-block-components-main .wcs-recurring-totals-panel__details{padding:0 16px}.wcs-recurring-totals-panel__details .wc-block-components-panel__button,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:focus,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:hover{font-size:.875em}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:first-child{margin-top:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:last-child{margin-bottom:0}.wcs-recurring-totals-panel__details .wcs-recurring-totals-panel__details-total .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals__subscription-length{float:left}
|
||||
.wc-block-components-local-pickup-rates-control .wc-block-components-local-pickup-select:not(:last-child){margin-bottom:16px}
|
||||
|
||||
|
|
|
|||
|
|
@ -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', 'wc-blocks-checkout', 'wc-blocks-data-store', 'wc-price-format', 'wc-settings', 'wp-data', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '32bfa486d902830f29c7');
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
.wcs-recurring-totals-panel{padding:1em 0 0;position:relative}.wcs-recurring-totals-panel:after{border-style:solid;border-width:1px 0;bottom:0;content:"";display:block;left:0;opacity:.3;pointer-events:none;position:absolute;right:0;top:0}.wcs-recurring-totals-panel+.wcs-recurring-totals-panel:after{border-top-width:0}.wcs-recurring-totals-panel .wc-block-components-panel .wc-block-components-totals-item{padding-left:0;padding-right:0}.wcs-recurring-totals-panel .wc-block-components-totals-item__label:first-letter{text-transform:capitalize}.wcs-recurring-totals-panel .wcs-recurring-totals-panel__title .wc-block-components-totals-item__label{font-weight:500}.wcs-recurring-totals-panel__title{margin:0}.wc-block-components-main .wcs-recurring-totals-panel__details{padding:0 16px}.wcs-recurring-totals-panel__details .wc-block-components-panel__button,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:focus,.wcs-recurring-totals-panel__details .wc-block-components-panel__button:hover{font-size:.875em}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:first-child{margin-top:0}.wcs-recurring-totals-panel__details .wc-block-components-panel__content>.wc-block-components-totals-item:last-child{margin-bottom:0}.wcs-recurring-totals-panel__details .wcs-recurring-totals-panel__details-total .wc-block-components-totals-item__label{font-weight:700}.wcs-recurring-totals__subscription-length{float:right}
|
||||
.wc-block-components-local-pickup-rates-control .wc-block-components-local-pickup-select:not(:last-child){margin-bottom:16px}
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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!important}
|
||||
|
|
@ -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!important}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.wcsg-gifting-to-container-editing{display:flex;gap:5px;margin-top:10px;min-width:210px}.wcsg-gifting-to-container-editing .wc-block-components-text-input{flex-grow:1;margin-top:0}.wcsg-gifting-to-container-editing .wp-element-button.gifting-update-button:not(.is-link){min-height:unset;padding:0 var(--xs,20px)}.wcsg-gifting-to-container-editing .components-base-control__field{margin-bottom:0}.wcsg-gifting-to-container-editing .wc-block-components-text-input input:-webkit-autofill{padding-top:.5em}.wcsg-gifting-to-container-editing .has-error .components-text-control__input{color:var(--wc-red,#cc1818)}.wp-block-woocommerce-mini-cart-contents .wcsg-gifting-to-container-editing{flex-direction:column}.wp-block-woocommerce-mini-cart-contents .gifting-update-button{height:40px}.wcsg-gifting-to-container-view{align-items:center;display:flex;flex-wrap:wrap;gap:5px;max-width:100%}.wcsg-gifting-to-container-view span{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word;gap:5px}.wcsg-gifting-to-container-view .components-button.is-link{color:var(--wp--preset--color--contrast);flex-shrink:0;font-size:medium}.wc-block-checkout,.wc-block-components-product-details__gifting-to{align-items:center;display:flex;flex-wrap:wrap;max-width:100%}.wc-block-checkout .wc-block-components-product-details__name:after,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__name:after{content:" "}.wc-block-checkout .wc-block-components-product-details__value,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__value{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word}.wcsg-block-recipient-container{font-size:var(--wp--preset--font-size--small,14px);line-height:20px}.wcsg-block-recipient-container .components-checkbox-control__label{font-size:var(--wp--preset--font-size--small,14px);margin-bottom:0}.wc-block-cart .wc-block-components-product-details__gifting-to,.wc-block-cart .wc-block-components-product-details__gifting-to-hidden,.wc-block-cart .wc-block-components-product-details__item-key,.wc-block-checkout .wc-block-components-product-details__gifting-to-hidden,.wc-block-checkout .wc-block-components-product-details__item-key,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to-hidden,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__item-key{display:none}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<?php return array('dependencies' => array('react', 'react-dom', 'wc-blocks-checkout', 'wp-components', 'wp-data', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'cf4dd71c0c7418b591fd');
|
||||
|
|
@ -0,0 +1 @@
|
|||
.wcsg-gifting-to-container-editing{display:flex;gap:5px;margin-top:10px;min-width:210px}.wcsg-gifting-to-container-editing .wc-block-components-text-input{flex-grow:1;margin-top:0}.wcsg-gifting-to-container-editing .wp-element-button.gifting-update-button:not(.is-link){min-height:unset;padding:0 var(--xs,20px)}.wcsg-gifting-to-container-editing .components-base-control__field{margin-bottom:0}.wcsg-gifting-to-container-editing .wc-block-components-text-input input:-webkit-autofill{padding-top:.5em}.wcsg-gifting-to-container-editing .has-error .components-text-control__input{color:var(--wc-red,#cc1818)}.wp-block-woocommerce-mini-cart-contents .wcsg-gifting-to-container-editing{flex-direction:column}.wp-block-woocommerce-mini-cart-contents .gifting-update-button{height:40px}.wcsg-gifting-to-container-view{align-items:center;display:flex;flex-wrap:wrap;gap:5px;max-width:100%}.wcsg-gifting-to-container-view span{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word;gap:5px}.wcsg-gifting-to-container-view .components-button.is-link{color:var(--wp--preset--color--contrast);flex-shrink:0;font-size:medium}.wc-block-checkout,.wc-block-components-product-details__gifting-to{align-items:center;display:flex;flex-wrap:wrap;max-width:100%}.wc-block-checkout .wc-block-components-product-details__name:after,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__name:after{content:" "}.wc-block-checkout .wc-block-components-product-details__value,.wc-block-components-product-details__gifting-to .wc-block-components-product-details__value{align-items:center;display:inline-flex;max-width:100%;word-break:break-all;word-wrap:break-word}.wcsg-block-recipient-container{font-size:var(--wp--preset--font-size--small,14px);line-height:20px}.wcsg-block-recipient-container .components-checkbox-control__label{font-size:var(--wp--preset--font-size--small,14px);margin-bottom:0}.wc-block-cart .wc-block-components-product-details__gifting-to,.wc-block-cart .wc-block-components-product-details__gifting-to-hidden,.wc-block-cart .wc-block-components-product-details__item-key,.wc-block-checkout .wc-block-components-product-details__gifting-to-hidden,.wc-block-checkout .wc-block-components-product-details__item-key,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__gifting-to-hidden,.wp-block-woocommerce-mini-cart-contents .wc-block-components-product-details__item-key{display:none}
|
||||
File diff suppressed because one or more lines are too long
103
changelog.txt
103
changelog.txt
|
|
@ -1,9 +1,110 @@
|
|||
*** WooCommerce Subscriptions Changelog ***
|
||||
|
||||
2026-01-06 - version 8.3.0
|
||||
* Add: Support for ajax add-to-cart flows from the single product page, when upgrading or downgrading subscriptions.
|
||||
* Add: Enhanced functionality from the WooCommerce Subscription Downloads plugin has now been integrated directly into WooCommerce Subscriptions.
|
||||
* Fix: Ensured the subject line for the "Customer Renewal Invoice" email correctly updates.
|
||||
|
||||
2025-12-22 - version 8.2.1
|
||||
* Fix: Subscription pricing strings now correctly display singular and plural billing intervals like "$10 / month" and "$10 every 3 months" in cart and checkout totals.
|
||||
* Fix: Display all recurring coupon names in the totals section of classic cart and checkout when multiple recurring coupons are applied.
|
||||
* Fix: Payment method selector options on Subscriptions list page are now properly displaying the payment method name for gateways providing multiple payment methods.
|
||||
* Fix: PayPal Standard plugin IPN renewal orders not being created for failed payment retries.
|
||||
* Fix: Prevent fatal errors on shop pages when special characters like % are used in a product name.
|
||||
* Fix: Prevent an email validation error from briefly and inadvertently flashing on the screen when the "this is a gift" checkbox is used.
|
||||
|
||||
2025-12-10 - version 8.2.0
|
||||
* Fix: Various accessibility issues.
|
||||
* Added button roles.
|
||||
* Added labels to remove product links.
|
||||
* Adding missing header labels on related orders table.
|
||||
* Prevented various hidden elements from being exposed to screen readers.
|
||||
* Fixed focus problems for the renewal dialog.
|
||||
* Fixed focus when closing modals.
|
||||
* Fixed toggle state change using spacebar.
|
||||
* Fixed label for sign up now button.
|
||||
* Fixed toggle aria-label text to reflect state.
|
||||
* Added aria-haspopup attribute to buttons that open modals.
|
||||
* Fixed link for view order aria-label.
|
||||
* Fixed renew now button text and aria-label.
|
||||
* Added aria-modal and role attributes to modals and dialogs.
|
||||
* Fix: Resubscribe button now appears correctly for cancelled limited subscriptions.
|
||||
* Fix: Missing translation for pricing string on languages without plural form.
|
||||
* Fix: Customized subscriptions links in WooCommerce emails now work correctly.
|
||||
* Fix: Possible fatal errors when switching grouped subscriptions.
|
||||
* Fix: Prevent unnecessary log entries related to gifted subscriptions.
|
||||
* Fix: Make it easier for translation plugins to respect individual customer language preferences when sending subscription emails.
|
||||
* Fix: Prevent recurring local pickup options from displaying for virtual subscriptions when the cart contains physical products.
|
||||
* Fix: Prevent shipping summary from displaying on Blocks Checkout for virtual subscriptions.
|
||||
* Fix: Item price in blocks checkout is correctly displayed without "due today" words for items with zero sign up fee.
|
||||
|
||||
2025-11-13 - version 8.1.0
|
||||
* Fix: Prevent a fatal error that can occur when previewing emails in WooCommerce email settings.
|
||||
* Fix: Prevent technical subscription-specific discount types from appearing in the coupon edit UI.
|
||||
* Fix: Add admin notice and debug tool to recreate the payment retry database table when it's missing.
|
||||
* Fix: Prevent resubscribe and reactivate buttons from showing for subscriptions containing disabled, deleted, draft, or limited products.
|
||||
* Fix: Prevent error that blocks the subscriptions list from loading when gifted subscriptions are purchased with certain payment methods.
|
||||
* Fix: Ensure gift recipient email address is displayed in the order confirmation email sent to the purchaser.
|
||||
* Fix: Prevent pickup location options from displaying incorrectly alongside shipping methods on the Blocks Checkout page.
|
||||
* Fix: Fix invalid HTML tags that cause fatal errors when using themes with WordPress Interactivity API.
|
||||
* Fix: Add password reset links to the new account email template for gifted subscription recipients.
|
||||
* Tweak: Improve the gift recipient email input field on Blocks Cart and Mini Cart to match the style of other checkout fields.
|
||||
* Dev: Improve housekeeping of output buffers when handling requests to change subscription payment methods.
|
||||
* Dev: Improve type safety in relation to the `wcs_get_subscription()` function.
|
||||
|
||||
2025-10-15 - version 8.0.0
|
||||
* Add: Blocks Checkout now displays a single shipping method selection for both initial and recurring carts, similar to Classic Checkout. This can be disabled using the `wcs_cart_totals_shipping_html_price_only` filter.
|
||||
* Fix: Resubscribing to a limited subscription from the product page will now reactivate the existing subscription.
|
||||
* Fix: "New initial order - Recipient" email template preview no longer causes critical error.
|
||||
* Fix: Shipping value on blocks checkout no longer shows as "Free" when no shipping method is available.
|
||||
* Dev: Improved data telemetry which is sent to WooCommerce:
|
||||
* WooCommerce Subscriptions setting values
|
||||
* Order totals of subscription-related orders by order type and payment gateway, aggregated by month
|
||||
* Number of subscription-related orders by order type and payment gateway, aggregated by month
|
||||
* Number of products in parent subscription orders, aggregated by month
|
||||
* Number of non-zero value parent subscription orders, aggregated by month
|
||||
* Number of subscription products by billing interval
|
||||
* Number of giftable subscription products
|
||||
* Number of subscriptions by payment method, billing period, and renewal type
|
||||
* Number of gifted subscriptions
|
||||
* Number of subscribers
|
||||
* Purchase event for subscription products
|
||||
* Exclude orders with a "processing" status from the "subscription_orders" telemetry data.
|
||||
|
||||
2025-09-16 - version 7.9.0
|
||||
* Add: REST API endpoint to retrieve subscriptions associated with a specific order ID.
|
||||
* Fix: Keep subscription with successful renewals active when parent order fails.
|
||||
* Fix: Error when renewing subscriptions with downloadable gifts.
|
||||
* Fix: Improve performance by loading assets only when necessary.
|
||||
* Fix: "Update cart" button being always enabled.
|
||||
* Fix: Gifting recipient email layout issues when using a long e-mail address.
|
||||
* Fix: Gifting checkbox not showing up on blocks cart when using WooCommerce 10.2+.
|
||||
* Fix: Deleting a gifted subscription recipient will not delete their subscriptions, they will be moved to the subscription purchaser instead.
|
||||
* Dev: The `wcs_get_subscriptions()` and `wcs_get_subscriptions_for_order()` functions are now stricter, and only return instances of `WC_Subscription`.
|
||||
|
||||
2025-09-02 - version 7.8.2
|
||||
* Fix: Error when updating/renewing subscriptions containing a pseudo coupon.
|
||||
* Fix: Fix gifting recipient notice being shown for non gifting orders.
|
||||
|
||||
2025-08-25 - version 7.8.1
|
||||
* Fix: Fix checkout button default label.
|
||||
* Fix: Fix error when WP CLI is enabled in some environments.
|
||||
|
||||
2025-08-19 - version 7.8.0
|
||||
* Add: Native support for subscriptions gifting. Gifting for WooCommerce Subscriptions extension is no longer required.
|
||||
* Add: Enable subscriptions gifting storewide and per product.
|
||||
* 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.
|
||||
* Fix: Fix order renewal errors when using some plugins.
|
||||
* Fix: Fix order renewal errors when using some plugins.
|
||||
* Fix: Prevent fatal errors when loading or deleting subscriptions with corrupted date values stored in a database.
|
||||
* Fix: Prevent fatal errors when trying to save invalid date values and display better error messages instead.
|
||||
* Fix: Fix broken blocks and javascript translations.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
/**
|
||||
* WCS_Admin_Assets Class
|
||||
*
|
||||
* Handles admin assets (scripts and styles) for WooCommerce Subscriptions.
|
||||
*
|
||||
* @package WooCommerce Subscriptions/Admin
|
||||
*/
|
||||
class WCS_Admin_Assets {
|
||||
|
||||
/**
|
||||
* Initialize the tour handler
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue required scripts and styles
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/admin.asset.php' );
|
||||
$script_asset = file_exists( $script_asset_path )
|
||||
? require $script_asset_path
|
||||
: array(
|
||||
'dependencies' => array(
|
||||
'react',
|
||||
'wc-blocks-checkout',
|
||||
'wc-price-format',
|
||||
'wc-settings',
|
||||
'wp-element',
|
||||
'wp-i18n',
|
||||
'wp-plugins',
|
||||
),
|
||||
'version' => WC_Subscriptions::$version,
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wcs-admin',
|
||||
plugins_url( '/build/admin.js', WC_Subscriptions::$plugin_file ),
|
||||
$script_asset['dependencies'],
|
||||
$script_asset['version'],
|
||||
true
|
||||
);
|
||||
|
||||
wp_enqueue_style(
|
||||
'wcs-admin',
|
||||
plugins_url( '/build/style-admin.css', WC_Subscriptions::$plugin_file ),
|
||||
array( 'wp-components' ),
|
||||
$script_asset['version']
|
||||
);
|
||||
|
||||
wp_set_script_translations(
|
||||
'wcs-admin',
|
||||
'woocommerce-subscriptions',
|
||||
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'languages'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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§ion=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§ion=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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
* -- Subscription specific --
|
||||
* GET /subscriptions/status
|
||||
* GET /subscriptions/<subscription_id>/orders
|
||||
* GET /orders/<order_id>/subscriptions
|
||||
* POST /orders/<order_id>/subscriptions
|
||||
*
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
|
@ -61,15 +63,25 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
) );
|
||||
|
||||
register_rest_route( $this->namespace, "/orders/(?P<id>[\d]+)/{$this->rest_base}", array(
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
"/orders/(?P<id>[\d]+)/{$this->rest_base}",
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_subscriptions_from_order' ),
|
||||
'permission_callback' => array( $this, 'create_item_permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
) );
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_order_subscriptions' ),
|
||||
'permission_callback' => array( $this, 'get_items_permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_subscriptions_from_order' ),
|
||||
'permission_callback' => array( $this, 'create_item_permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -206,6 +218,76 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller {
|
|||
return apply_filters( 'wcs_rest_subscription_orders_response', $response, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the /orders/[id]/subscriptions response.
|
||||
*
|
||||
* @since 7.9.0
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
* @return WP_Error|WP_REST_Response $response The response or an error if one occurs.
|
||||
*/
|
||||
public function get_order_subscriptions( $request ) {
|
||||
$order_id = absint( $request['id'] );
|
||||
|
||||
if ( empty( $order_id ) ) {
|
||||
return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return new WP_Error(
|
||||
'woocommerce_rest_invalid_order_id',
|
||||
// translators: %d is the order ID.
|
||||
sprintf( __( 'Failed to load order object with the ID %d.', 'woocommerce-subscriptions' ), $order_id ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Build arguments for wcs_get_subscriptions_for_order()x
|
||||
$args = array(
|
||||
'order_type' => 'any', // Return all subscriptions for the order.
|
||||
);
|
||||
|
||||
// Set arguments from request.
|
||||
if ( ! empty( $request['orderby'] ) ) {
|
||||
$args['orderby'] = $request['orderby'];
|
||||
}
|
||||
|
||||
if ( ! empty( $request['order'] ) ) {
|
||||
$args['order'] = $request['order'];
|
||||
}
|
||||
|
||||
// Map standard request parameters to wcs_get_subscriptions_for_order() arguments.
|
||||
if ( ! empty( $request['customer'] ) ) {
|
||||
$args['customer_id'] = $request['customer'];
|
||||
}
|
||||
|
||||
if ( ! empty( $request['status'] ) ) {
|
||||
$args['subscription_status'] = $request['status'];
|
||||
}
|
||||
|
||||
$subscriptions = wcs_get_subscriptions_for_order( $order, $args );
|
||||
|
||||
$response_data = array();
|
||||
|
||||
foreach ( $subscriptions as $subscription ) {
|
||||
if ( is_a( $subscription, 'WC_Subscription' ) && ! wc_rest_check_post_permissions( 'shop_subscription', 'read', $subscription->get_id() ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$response = $this->prepare_object_for_response( $subscription, $request );
|
||||
$response_data[] = $this->prepare_response_for_collection( $response );
|
||||
}
|
||||
|
||||
$response = rest_ensure_response( $response_data );
|
||||
// Pagination is not fully supported, so we manually set the total.
|
||||
$response->header( 'X-WP-Total', count( $response_data ) );
|
||||
$response->header( 'X-WP-TotalPages', 1 );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides WC_REST_Orders_Controller::get_order_statuses() so that subscription statuses are
|
||||
* validated correctly.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
<?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',
|
||||
'woocommerce_subscriptions_downloads_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' );
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,10 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
if ( ! empty( $post->post_type ) && ! empty( $post->ID ) && 'shop_subscription' == $post->post_type ) {
|
||||
$subscription = wcs_get_subscription( $post->ID );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response->data['billing_period'] = $subscription->get_billing_period();
|
||||
$response->data['billing_interval'] = $subscription->get_billing_interval();
|
||||
|
||||
|
|
@ -292,8 +296,13 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
return new WP_Error( 'woocommerce_rest_invalid_shop_subscription_id', __( 'Invalid subscription id.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$this->post_type = 'shop_order';
|
||||
$subscription = wcs_get_subscription( $id );
|
||||
$this->post_type = 'shop_order';
|
||||
$subscription = wcs_get_subscription( $id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return new WP_Error( 'woocommerce_rest_invalid_shop_subscription_id', __( 'Invalid subscription id.', 'woocommerce-subscriptions' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$subscription_orders = $subscription->get_related_orders();
|
||||
|
||||
$orders = array();
|
||||
|
|
@ -371,6 +380,10 @@ class WC_REST_Subscriptions_V1_Controller extends WC_REST_Orders_V1_Controller {
|
|||
// In particular, the update_post_meta() called while _stripe_card_id is updated to _stripe_source_id
|
||||
$subscription = wcs_get_subscription( $subscription->get_id() );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
throw new WC_REST_Exception( 'woocommerce_rest_payment_update_failed', __( 'Subscription payment method could not be set updated due to technical issues.', 'woocommerce-subscriptions' ), 500 );
|
||||
}
|
||||
|
||||
if ( isset( $payment_method_meta[ $payment_method ] ) ) {
|
||||
$payment_method_meta = $payment_method_meta[ $payment_method ];
|
||||
|
||||
|
|
|
|||
|
|
@ -702,12 +702,16 @@ class WC_REST_Subscriptions_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
$subscription->add_item( $item );
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Fetch a fresh instance of the subscription because the current instance has an empty line item cache generated before we had copied the line items.
|
||||
* Fetching a new instance will ensure the line items are used when calculating totals.
|
||||
*/
|
||||
$subscription = wcs_get_subscription( $subscription->get_id() );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
throw new Exception( __( 'There was a problem completing this request. The subscription may have been deleted by another process.', 'woocommerce-subscriptions' ) );
|
||||
}
|
||||
|
||||
$subscription->calculate_totals();
|
||||
|
||||
/**
|
||||
|
|
@ -719,7 +723,13 @@ class WC_REST_Subscriptions_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
*/
|
||||
do_action( "woocommerce_rest_insert_{$this->post_type}_object", $subscription, $request, true );
|
||||
|
||||
$response = $this->prepare_object_for_response( wcs_get_subscription( $subscription->get_id() ), $request );
|
||||
$fresh_subscription = wcs_get_subscription( $subscription->get_id() );
|
||||
|
||||
if ( ! $fresh_subscription ) {
|
||||
throw new Exception( __( 'There was a problem completing this request. The subscription may have been deleted by another process.', 'woocommerce-subscriptions' ) );
|
||||
}
|
||||
|
||||
$response = $this->prepare_object_for_response( $fresh_subscription, $request );
|
||||
$subscriptions[] = $this->prepare_response_for_collection( $response );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +117,13 @@ class WC_Subscriptions_Dependency_Manager {
|
|||
|
||||
$this->wc_version_cached = true;
|
||||
|
||||
// Try to get version from transient first
|
||||
$this->wc_active_version = get_transient( 'wcs_woocommerce_active_version' );
|
||||
|
||||
if ( false !== $this->wc_active_version ) {
|
||||
return $this->wc_active_version;
|
||||
}
|
||||
|
||||
// Load plugin.php if it's not already loaded.
|
||||
if ( ! function_exists( 'is_plugin_active' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
|
|
@ -122,9 +146,15 @@ class WC_Subscriptions_Dependency_Manager {
|
|||
|
||||
if ( $is_woocommerce && is_plugin_active( $plugin_slug ) ) {
|
||||
$this->wc_active_version = $plugin_data['Version'];
|
||||
break; // Found it, no need to continue looping
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result in a transient for 1 hour
|
||||
if ( ! empty( $this->wc_active_version ) ) {
|
||||
set_transient( 'wcs_woocommerce_active_version', $this->wc_active_version, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $this->wc_active_version;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce_Subscriptions\Internal\Telemetry\Events as WC_Tracks_Events;
|
||||
|
||||
/**
|
||||
* @method static WC_Subscriptions_Plugin instance()
|
||||
*/
|
||||
|
|
@ -32,12 +34,19 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
|
|||
WCS_Call_To_Action_Button_Text_Manager::init();
|
||||
WCS_Subscriber_Role_Manager::init();
|
||||
WCS_Upgrade_Notice_Manager::init();
|
||||
WCS_Admin_Assets::init();
|
||||
|
||||
$tracks_events = new WC_Tracks_Events();
|
||||
$tracks_events->setup();
|
||||
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
new WC_Subscriptions_CLI();
|
||||
}
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_show_welcome_message' ) );
|
||||
add_action( 'plugins_loaded', array( $this, 'init_gifting' ) );
|
||||
add_action( 'plugins_loaded', array( $this, 'init_downloads' ) );
|
||||
add_action( 'admin_notices', array( WC_Subscription_Downloads_Settings::class, 'add_notice_about_bundled_feature' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -232,4 +241,142 @@ 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_Admin_Order::init();
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to initialize additional downloads functionality.
|
||||
*
|
||||
* This functionality makes it possible to link downloadable products with a subscription
|
||||
* product. Purchasers of the subscription product then automatically get access to the files associated with downloadable product.
|
||||
*
|
||||
* Previously, this functionality existed as a standalone plugin (WooCommerce Subscription Downnloads) and so,
|
||||
* before initializing, we try to determine if the standalone plugin is active and has already loaded (if it is
|
||||
* active, we do not proceed).
|
||||
*/
|
||||
public function init_downloads() {
|
||||
if (
|
||||
$this->is_plugin_being_activated( 'woocommerce-subscription-downloads' )
|
||||
|| class_exists( WC_Subscription_Downloads::class, false )
|
||||
) {
|
||||
if ( class_exists( WC_Subscription_Downloads::class, false ) ) {
|
||||
// Will show the welcome announcement if the standalone plugin is active and the welcome announcement has not been dismissed.
|
||||
if ( ! WC_Subscription_Downloads_Admin_Welcome_Announcement::is_welcome_announcement_dismissed() ) {
|
||||
WC_Subscription_Downloads_Admin_Welcome_Announcement::init();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
WC_Subscription_Downloads::setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to determine if the specified plugin is being activated.
|
||||
*
|
||||
* 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 ( ! is_countable( $args ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
( count( $args ) < 3
|
||||
|| 'plugin' !== $args[0]
|
||||
|| 'activate' !== $args[1] )
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
* @since 2.0
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
|
@ -60,6 +62,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 ) {
|
||||
|
|
@ -152,7 +155,7 @@ class WCS_API {
|
|||
* @return boolean
|
||||
*/
|
||||
protected static function is_orders_api_request() {
|
||||
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST || empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
|
||||
if ( ! Constants::is_true( 'REST_REQUEST' ) || empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class WCS_Autoloader extends WCS_Core_Autoloader {
|
|||
'wcs_webhooks' => true,
|
||||
'wcs_auth' => true,
|
||||
'wcs_upgrade_notice_manager' => true,
|
||||
'wcs_admin_assets' => true,
|
||||
'wc_subscriptions_cli' => true,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,32 +30,32 @@ class WCS_Call_To_Action_Button_Text_Manager {
|
|||
public static function add_settings( $settings ) {
|
||||
$button_text_settings = array(
|
||||
array(
|
||||
'name' => __( 'Button Text', 'woocommerce-subscriptions' ),
|
||||
'name' => __( 'Button text', 'woocommerce-subscriptions' ),
|
||||
'type' => 'title',
|
||||
'desc' => '',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_button_text',
|
||||
),
|
||||
array(
|
||||
'name' => __( 'Add to Cart Button Text', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'A product displays a button with the text "Add to cart". By default, a subscription changes this to "Sign up now". You can customise the button text for subscriptions here.', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'A product displays a button with the text "Add to cart". You can customise the button text for subscriptions here.', 'woocommerce-subscriptions' ),
|
||||
'tip' => '',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text',
|
||||
'css' => 'min-width:150px;',
|
||||
'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' ),
|
||||
'desc' => __( 'Use this field to customise the text displayed on the checkout button when an order contains a subscription. Normally the checkout submission button displays "Place order". When the cart contains a subscription, this is changed to "Sign up now".', 'woocommerce-subscriptions' ),
|
||||
'desc' => __( 'Use this field to customise the text displayed on the checkout button when an order contains a subscription.', 'woocommerce-subscriptions' ),
|
||||
'tip' => '',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_order_button_text',
|
||||
'css' => 'min-width:150px;',
|
||||
'default' => __( 'Sign up now', 'woocommerce-subscriptions' ),
|
||||
'default' => __( 'Place order', 'woocommerce-subscriptions' ),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Sign up now', 'woocommerce-subscriptions' ),
|
||||
'placeholder' => __( 'Place order', 'woocommerce-subscriptions' ),
|
||||
),
|
||||
array(
|
||||
'type' => 'sectionend',
|
||||
|
|
|
|||
|
|
@ -357,7 +357,12 @@ class WCS_Limited_Recurring_Coupon_Manager {
|
|||
$has_limited_coupon = false;
|
||||
|
||||
if ( $change_payment && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) ) ) {
|
||||
$subscription = wcs_get_subscription( $change_payment );
|
||||
$subscription = wcs_get_subscription( $change_payment );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return $gateways;
|
||||
}
|
||||
|
||||
$has_limited_coupon = self::order_has_limited_recurring_coupon( $subscription );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,10 @@ class WCS_Subscriber_Role_Manager {
|
|||
),
|
||||
);
|
||||
|
||||
WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_button_text', $role_settings, 'multiple_settings', 'sectionend' );
|
||||
if ( ! WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_button_text', $role_settings, 'multiple_settings', 'sectionend' ) ) {
|
||||
$settings = array_merge( $settings, $role_settings );
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,9 +138,7 @@ class WCS_Webhooks {
|
|||
throw new \Exception( 'The Legacy REST API plugin is not installed on this site. More information: https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/ ' );
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
WC()->api->WC_API_Subscriptions->register_routes( array() );
|
||||
// @phpstan-ignore-next-line
|
||||
$payload = WC()->api->WC_API_Subscriptions->get_subscription( $resource_id );
|
||||
break;
|
||||
case 'wp_api_v1':
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -48,6 +48,18 @@ abstract class WCS_Table_Maker {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the schema option and recreates the tables.
|
||||
*
|
||||
* This forces the table schema to be regenerated by removing the stored
|
||||
* schema version and triggering the table registration process.
|
||||
*/
|
||||
public function recreate_tables() {
|
||||
delete_option( $this->get_schema_option_name() );
|
||||
|
||||
$this->register_tables();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table The name of the table
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
),
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ class WCS_Admin_Post_Types {
|
|||
'wcs-unknown-order-info-wrapper',
|
||||
esc_url( 'https://woocommerce.com/document/subscriptions/store-manager-guide/#section-19' ),
|
||||
// translators: Placeholder is a <br> HTML tag.
|
||||
wcs_help_tip( sprintf( __( "This subscription couldn't be loaded from the database. %s Click to learn more.", 'woocommerce-subscriptions' ), '</br>' ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
wcs_help_tip( sprintf( __( "This subscription couldn't be loaded from the database. %s Click to learn more.", 'woocommerce-subscriptions' ), '<br>' ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -711,12 +711,12 @@ class WCS_Admin_Post_Types {
|
|||
$tooltip_classes = 'woocommerce-help-tip';
|
||||
|
||||
if ( $datetime->getTimestamp() < time() ) {
|
||||
$tooltip_message .= __( '<b>Subscription payment overdue.</b></br>', 'woocommerce-subscriptions' );
|
||||
$tooltip_message .= '<b>' . esc_html__( 'Subscription payment overdue.', 'woocommerce-subscriptions' ) . '</b><br>';
|
||||
$tooltip_classes .= ' wcs-payment-overdue';
|
||||
}
|
||||
|
||||
if ( $subscription->payment_method_supports( 'gateway_scheduled_payments' ) && ! $subscription->is_manual() ) {
|
||||
$tooltip_message .= __( 'This date should be treated as an estimate only. The payment gateway for this subscription controls when payments are processed.</br>', 'woocommerce-subscriptions' );
|
||||
$tooltip_message .= esc_html__( 'This date should be treated as an estimate only. The payment gateway for this subscription controls when payments are processed.', 'woocommerce-subscriptions' ) . '<br>';
|
||||
$tooltip_classes .= ' wcs-offsite-renewal';
|
||||
}
|
||||
|
||||
|
|
@ -1171,7 +1171,7 @@ class WCS_Admin_Post_Types {
|
|||
|
||||
// @phpstan-ignore property.notFound
|
||||
foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) {
|
||||
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id === $gateway_id ? 'selected' : '' ) . '>' . esc_html( $gateway->title ) . '</option>';
|
||||
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id === $gateway_id ? 'selected' : '' ) . '>' . esc_html( method_exists( $gateway, 'get_title' ) ? $gateway->get_title() : $gateway->title ) . '</option>';
|
||||
}
|
||||
echo '<option value="_manual_renewal">' . esc_html__( 'Manual Renewal', 'woocommerce-subscriptions' ) . '</option>';
|
||||
?>
|
||||
|
|
@ -1503,9 +1503,20 @@ class WCS_Admin_Post_Types {
|
|||
|
||||
foreach ( $subscription_ids as $subscription_id ) {
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
$note = _x( 'Subscription status changed by bulk edit:', 'Used in order note. Reason why status changed.', 'woocommerce-subscriptions' );
|
||||
|
||||
$note = _x( 'Subscription status changed by bulk edit:', 'Used in order note. Reason why status changed.', 'woocommerce-subscriptions' );
|
||||
|
||||
try {
|
||||
if ( ! $subscription ) {
|
||||
throw new Exception(
|
||||
sprintf(
|
||||
// Translators: 1: subscription ID.
|
||||
__( 'Subscription with ID %1$d does not exist and could not be updated.', 'woocommerce-subscriptions' ),
|
||||
$subscription_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'cancelled' === $new_status ) {
|
||||
$subscription->cancel_order( $note );
|
||||
} else {
|
||||
|
|
@ -1542,6 +1553,11 @@ class WCS_Admin_Post_Types {
|
|||
|
||||
foreach ( $subscription_ids as $id ) {
|
||||
$subscription = wcs_get_subscription( $id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subscription->delete( $force_delete );
|
||||
$updated_subscription = wcs_get_subscription( $id );
|
||||
|
||||
|
|
@ -1571,7 +1587,13 @@ class WCS_Admin_Post_Types {
|
|||
|
||||
foreach ( $subscription_ids as $id ) {
|
||||
if ( $use_crud_method ) {
|
||||
$data_store->untrash_order( wcs_get_subscription( $id ) );
|
||||
$subscription = wcs_get_subscription( $id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data_store->untrash_order( $subscription );
|
||||
} else {
|
||||
wp_untrash_post( $id );
|
||||
}
|
||||
|
|
@ -1868,7 +1890,6 @@ class WCS_Admin_Post_Types {
|
|||
* @return string[] $pieces Updated associative array of clauses for the query.
|
||||
*/
|
||||
private function orders_table_clauses_low_performance( $pieces ) {
|
||||
// @phpstan-ignore-next-line
|
||||
$order_datastore = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::class );
|
||||
$order_table = $order_datastore::get_orders_table_name();
|
||||
$meta_table = $order_datastore::get_meta_table_name();
|
||||
|
|
@ -1899,7 +1920,6 @@ class WCS_Admin_Post_Types {
|
|||
private function orders_table_clauses_high_performance( $pieces ) {
|
||||
global $wpdb;
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$order_datastore = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::class );
|
||||
$order_table = $order_datastore::get_orders_table_name();
|
||||
$meta_table = $order_datastore::get_meta_table_name();
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
sprintf( // Translators: The %1 placeholder is the translated order relationship ("Parent Order"), %2 placeholder is a <br> HTML tag.
|
||||
__( 'This %1$s couldn\'t be loaded from the database. %1$s Click to learn more.', 'woocommerce-subscriptions' ),
|
||||
esc_html( $relationship ),
|
||||
'</br>'
|
||||
'<br>'
|
||||
)
|
||||
);
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -17,24 +17,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
|
||||
class WC_Product_Subscription_Variation extends WC_Product_Variation {
|
||||
|
||||
/**
|
||||
* A way to access the old array property.
|
||||
*/
|
||||
protected $subscription_variation_level_meta_data;
|
||||
|
||||
/**
|
||||
* Create a simple subscription product object.
|
||||
*
|
||||
* @access public
|
||||
* @param mixed $product
|
||||
*/
|
||||
public function __construct( $product = 0 ) {
|
||||
|
||||
parent::__construct( $product );
|
||||
|
||||
$this->subscription_variation_level_meta_data = new WCS_Array_Property_Post_Meta_Black_Magic( $this->get_id() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic __get method for backwards compatibility. Map legacy vars to WC_Subscriptions_Product getters.
|
||||
*
|
||||
|
|
@ -43,19 +25,11 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
|
|||
*/
|
||||
public function __get( $key ) {
|
||||
|
||||
if ( 'subscription_variation_level_meta_data' === $key ) {
|
||||
$value = wcs_product_deprecated_property_handler( $key, $this );
|
||||
|
||||
wcs_deprecated_argument( __CLASS__ . '::$' . $key, '2.2.0', 'Product properties should not be accessed directly with WooCommerce 3.0+. Use the getter in WC_Subscriptions_Product instead.' );
|
||||
|
||||
$value = $this->subscription_variation_level_meta_data; // Behold, the horror that is the magic of WCS_Array_Property_Post_Meta_Black_Magic
|
||||
} else {
|
||||
|
||||
$value = wcs_product_deprecated_property_handler( $key, $this );
|
||||
|
||||
// No matching property found in wcs_product_deprecated_property_handler()
|
||||
if ( is_null( $value ) ) {
|
||||
$value = parent::__get( $key );
|
||||
}
|
||||
// No matching property found in wcs_product_deprecated_property_handler()
|
||||
if ( is_null( $value ) ) {
|
||||
$value = parent::__get( $key );
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
|
@ -89,7 +63,6 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
|
|||
/**
|
||||
* Get the add to cart button text
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function add_to_cart_text() {
|
||||
|
|
@ -106,7 +79,6 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
|
|||
/**
|
||||
* Get the add to cart button text for the single page
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function single_add_to_cart_text() {
|
||||
|
|
@ -116,7 +88,6 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
|
|||
/**
|
||||
* Checks if the variable product this variation belongs to is purchasable.
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable() {
|
||||
|
|
@ -128,12 +99,11 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {
|
|||
* Checks the product type to see if it is either this product's type or the parent's
|
||||
* product type.
|
||||
*
|
||||
* @access public
|
||||
* @param mixed $type Array or string of types
|
||||
* @return bool
|
||||
*/
|
||||
public function is_type( $type ) {
|
||||
if ( 'variation' == $type || ( is_array( $type ) && in_array( 'variation', $type ) ) ) {
|
||||
if ( 'variation' === $type || ( is_array( $type ) && in_array( 'variation', $type, true ) ) ) {
|
||||
return true;
|
||||
} else {
|
||||
return parent::is_type( $type );
|
||||
|
|
|
|||
|
|
@ -76,6 +76,32 @@ class WC_Product_Subscription extends WC_Product_Simple {
|
|||
return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the descriptive text for add-to-cart buttons.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function add_to_cart_description() {
|
||||
if ( $this->is_purchasable() && $this->is_in_stock() ) {
|
||||
// For accessibility reasons it is recommended that the aria-label is the same as, or else starts with, the
|
||||
// same text that is visible on the button itself.
|
||||
$text = sprintf(
|
||||
// Translators: %1$s: Pre-determined add-to-cart text 2: Product title.
|
||||
_x( '%1$s: “%2$s”', 'Add-to-cart button description', 'woocommerce-subscriptions' ),
|
||||
WC_Subscriptions_Product::get_add_to_cart_text(),
|
||||
$this->get_name()
|
||||
);
|
||||
} else {
|
||||
$text = sprintf(
|
||||
// Translators: %1$s: Product title.
|
||||
__( 'Read more about “%1$s”', 'woocommerce-subscriptions' ),
|
||||
$this->get_name()
|
||||
);
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_product_add_to_cart_description', $text, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the add to cart button text for the single page
|
||||
*
|
||||
|
|
|
|||
|
|
@ -44,6 +44,14 @@ class WC_Subscription extends WC_Order {
|
|||
*/
|
||||
private $editable;
|
||||
|
||||
/**
|
||||
* Stores if the subscription is in the payment completed flow.
|
||||
* Allowing subscriptions to be renewed even if they have limited products.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_payment_completed_flow = false;
|
||||
|
||||
/**
|
||||
* Extra data for this object. Name value pairs (name + default value). Used to add additional information to parent.
|
||||
*
|
||||
|
|
@ -340,7 +348,9 @@ class WC_Subscription extends WC_Order {
|
|||
break;
|
||||
case 'completed': // core WC order status mapped internally to avoid exceptions
|
||||
case 'active':
|
||||
if ( $this->payment_method_supports( 'subscription_reactivation' ) && $this->has_status( 'on-hold' ) ) {
|
||||
if ( ! $this->is_payment_completed_flow && $this->contains_unavailable_product() ) {
|
||||
$can_be_updated = false;
|
||||
} elseif ( $this->payment_method_supports( 'subscription_reactivation' ) && $this->has_status( 'on-hold' ) ) {
|
||||
// If the subscription's end date is in the past, it cannot be reactivated.
|
||||
$end_time = $this->get_time( 'end' );
|
||||
if ( 0 !== $end_time && $end_time < gmdate( 'U' ) ) {
|
||||
|
|
@ -415,6 +425,48 @@ class WC_Subscription extends WC_Order {
|
|||
return apply_filters( 'woocommerce_can_subscription_be_updated_to_' . $new_status, $can_be_updated, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the subscription contains an unavailable product.
|
||||
*
|
||||
* A product is considered unavailable if it is:
|
||||
* - Deleted (not found)
|
||||
* - Not published (draft, trash, private, etc.)
|
||||
*
|
||||
* Note: This method intentionally does NOT use is_purchasable() to avoid incorrectly
|
||||
* flagging limited products as unavailable. Limited products return is_purchasable() = false
|
||||
* for users with existing subscriptions, but they should still be available for resubscribe.
|
||||
* Functions like wcs_can_user_resubscribe_to() have specific logic to handle limited products
|
||||
* by checking if the user has an active subscription.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function contains_unavailable_product() {
|
||||
/** @var WC_Order_Item_Product $line_item */
|
||||
foreach ( $this->get_items() as $line_item ) {
|
||||
$product = $line_item->get_product();
|
||||
|
||||
// Product doesn't exist (deleted).
|
||||
if ( ! $product instanceof WC_Product ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the product is a subscription variation, use the parent product.
|
||||
if ( $product->is_type( 'subscription_variation' ) ) {
|
||||
$parent_product_id = $product->get_parent_id();
|
||||
$product = wc_get_product( $parent_product_id );
|
||||
}
|
||||
|
||||
// Check if product is published. Products with other statuses (draft, trash, private)
|
||||
// are not available for purchase or resubscribe.
|
||||
$product_status = $product->get_status();
|
||||
if ( 'publish' !== $product_status ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates status of the subscription
|
||||
*
|
||||
|
|
@ -1996,6 +2048,7 @@ class WC_Subscription extends WC_Order {
|
|||
* @param WC_Order $last_order
|
||||
*/
|
||||
public function payment_complete_for_order( $last_order ) {
|
||||
$this->is_payment_completed_flow = true;
|
||||
|
||||
// Clear the cached renewal payment counts
|
||||
if ( isset( $this->cached_payment_count['completed'] ) ) {
|
||||
|
|
@ -2028,30 +2081,49 @@ class WC_Subscription extends WC_Order {
|
|||
}
|
||||
|
||||
/**
|
||||
* When a payment fails, either for the original purchase or a renewal payment, this function processes it.
|
||||
* When related order fails, update the status of the related order and the subscription.
|
||||
* Related order can fail because of payment failure or because of other reasons.
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
|
||||
* @param string $new_status The new status to set for the subscription.
|
||||
* @param WC_Order|bool $related_order The related order that failed payment. False if no related order is found.
|
||||
* @since 7.9.0 Replaces payment_failed() method.
|
||||
*/
|
||||
public function payment_failed( $new_status = 'on-hold' ) {
|
||||
|
||||
// Make sure the last order's status is set to failed
|
||||
$last_order = $this->get_last_order( 'all', 'any' );
|
||||
|
||||
if ( false !== $last_order ) {
|
||||
$last_order->update_meta_data( self::RENEWAL_FAILED_META_KEY, wc_bool_to_string( true ) );
|
||||
|
||||
if ( false === $last_order->has_status( 'failed' ) ) {
|
||||
remove_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment' );
|
||||
$last_order->update_status( 'failed' );
|
||||
add_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment', 10, 3 );
|
||||
} else {
|
||||
// If we didn't update the status, save the order to make sure our self::RENEWAL_FAILED_META_KEY meta data is saved.
|
||||
$last_order->save();
|
||||
public function payment_failed_for_related_order( $new_status = 'on-hold', $related_order = false ) {
|
||||
if ( is_a( $related_order, 'WC_Order' ) ) {
|
||||
$related_order_was_modified = false;
|
||||
if ( wcs_order_contains_renewal( $related_order ) ) {
|
||||
$related_order->update_meta_data( self::RENEWAL_FAILED_META_KEY, wc_bool_to_string( true ) );
|
||||
$related_order_was_modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Log payment failure on order
|
||||
$this->add_order_note( __( 'Payment failed.', 'woocommerce-subscriptions' ) );
|
||||
if ( false === $related_order->has_status( 'failed' ) ) {
|
||||
remove_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment' );
|
||||
// No need to set $related_order_was_modified to true here because update status will save the changes.
|
||||
$related_order->update_status( 'failed' );
|
||||
add_filter( 'woocommerce_order_status_changed', 'WC_Subscriptions_Renewal_Order::maybe_record_subscription_payment', 10, 3 );
|
||||
} elseif ( $related_order_was_modified ) {
|
||||
// If we didn't update the status, save the order to make sure our self::RENEWAL_FAILED_META_KEY meta data is saved.
|
||||
$related_order->save();
|
||||
}
|
||||
|
||||
$this->add_order_note(
|
||||
sprintf(
|
||||
// translators: %d: related order ID
|
||||
__( 'Related order #%d failed.', 'woocommerce-subscriptions' ),
|
||||
$related_order->get_id()
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wc_get_logger()->debug(
|
||||
'WC_Subscription::payment_failed_for_related_order is called without a related order',
|
||||
array(
|
||||
'subscription_id' => $this->get_id(),
|
||||
'new_status' => $new_status,
|
||||
'related_order' => $related_order,
|
||||
'backtrace' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Allow a short circuit for plugins & payment gateways to force max failed payments exceeded
|
||||
// This also forces the new status to be 'cancelled' if the filter is applied or the subscription is pending-cancel
|
||||
|
|
@ -2068,11 +2140,23 @@ class WC_Subscription extends WC_Order {
|
|||
|
||||
do_action( 'woocommerce_subscription_payment_failed', $this, $new_status );
|
||||
|
||||
if ( false !== $last_order && wcs_order_contains_renewal( $last_order ) ) {
|
||||
do_action( 'woocommerce_subscription_renewal_payment_failed', $this, $last_order );
|
||||
if ( false !== $related_order && wcs_order_contains_renewal( $related_order ) ) {
|
||||
do_action( 'woocommerce_subscription_renewal_payment_failed', $this, $related_order );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a payment fails, either for the original purchase or a renewal payment, this function processes it.
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
|
||||
* @deprecated 7.9.0 - The method incorrectly assumes that the last order failed, and sometimes causes side effects.Use payment_failed_for_related_order instead.
|
||||
*/
|
||||
public function payment_failed( $new_status = 'on-hold' ) {
|
||||
// Make sure the last order's status is set to failed
|
||||
$last_order = $this->get_last_order( 'all', 'any' );
|
||||
$this->payment_failed_for_related_order( $new_status, $last_order );
|
||||
}
|
||||
|
||||
/*** Refund related functions are required for the Edit Order/Subscription screen, but they aren't used on a subscription ************/
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class WC_Subscriptions_Addresses {
|
|||
$actions['change_address'] = array(
|
||||
'url' => esc_url( add_query_arg( array( 'subscription' => $subscription->get_id() ), wc_get_endpoint_url( 'edit-address', 'shipping' ) ) ),
|
||||
'name' => __( 'Change address', 'woocommerce-subscriptions' ),
|
||||
'role' => 'link',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1088,21 +1088,11 @@ class WC_Subscriptions_Cart {
|
|||
self::set_cached_recurring_cart( $recurring_cart );
|
||||
|
||||
foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_key => $recurring_cart_package ) {
|
||||
$package_index = isset( $recurring_cart_package['package_index'] ) ? $recurring_cart_package['package_index'] : 0;
|
||||
$package = WC()->shipping->calculate_shipping_for_package( $recurring_cart_package );
|
||||
|
||||
$package_rates_match = false;
|
||||
if ( isset( $standard_packages[ $package_index ] ) ) {
|
||||
// phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
$package_rates_match = apply_filters( 'wcs_recurring_shipping_package_rates_match_standard_rates', $package['rates'] == $standard_packages[ $package_index ]['rates'], $package['rates'], $standard_packages[ $package_index ]['rates'], $recurring_cart_key );
|
||||
if ( self::package_rates_match_initial_rates( $standard_packages, $recurring_cart_package, $recurring_cart_key, $recurring_cart ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $package_rates_match ) {
|
||||
// The recurring package rates match the initial package rates, there won't be a selected shipping method for this recurring cart package move on to the next package.
|
||||
if ( apply_filters( 'wcs_cart_totals_shipping_html_price_only', true, $package, $recurring_cart ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$package = WC()->shipping->calculate_shipping_for_package( $recurring_cart_package );
|
||||
|
||||
// If the chosen shipping method is not available for this recurring cart package, display an error and unset the selected method.
|
||||
if ( ! isset( $package['rates'][ $shipping_methods[ $recurring_cart_package_key ] ] ) ) {
|
||||
|
|
@ -1127,6 +1117,47 @@ class WC_Subscriptions_Cart {
|
|||
self::set_cached_recurring_cart( $cached_recurring_cart );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the recurring package rates match the initial package rates.
|
||||
*
|
||||
* @param array $standard_packages The standard packages.
|
||||
* @param array $recurring_cart_package The recurring cart package.
|
||||
* @param string $recurring_cart_key The recurring cart key.
|
||||
* @param object $recurring_cart The recurring cart.
|
||||
* @return bool Whether the recurring package rates match the initial package rates.
|
||||
*/
|
||||
public static function package_rates_match_initial_rates( $standard_packages, $recurring_cart_package, $recurring_cart_key, $recurring_cart ) {
|
||||
$package_index = isset( $recurring_cart_package['package_index'] ) ? $recurring_cart_package['package_index'] : 0;
|
||||
$package = WC()->shipping->calculate_shipping_for_package( $recurring_cart_package );
|
||||
|
||||
$package_rates_match = false;
|
||||
if ( isset( $standard_packages[ $package_index ] ) ) {
|
||||
// Ignore item names during the comparison.
|
||||
$package_rates = array_map(
|
||||
[ __CLASS__, 'remove_item_names_from_rate' ],
|
||||
$package['rates']
|
||||
);
|
||||
$initial_rates = array_map(
|
||||
[ __CLASS__, 'remove_item_names_from_rate' ],
|
||||
$standard_packages[ $package_index ]['rates']
|
||||
);
|
||||
|
||||
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Order of array values is not important.
|
||||
$package_rates_match = apply_filters( 'wcs_recurring_shipping_package_rates_match_standard_rates', $package_rates == $initial_rates, $package['rates'], $standard_packages[ $package_index ]['rates'], $recurring_cart_key );
|
||||
}
|
||||
|
||||
if ( $package_rates_match ) {
|
||||
/**
|
||||
* The recurring package rates match the initial package rates, there won't be a selected shipping method for this recurring cart package move on to the next package.
|
||||
*
|
||||
* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
*/
|
||||
return apply_filters( 'wcs_cart_totals_shipping_html_price_only', true, $package, $recurring_cart );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the cart to see if it contains a specific product.
|
||||
*
|
||||
|
|
@ -1159,19 +1190,20 @@ class WC_Subscriptions_Cart {
|
|||
*/
|
||||
public static function cart_contains_other_subscription_products( $product_id ) {
|
||||
|
||||
$cart_contains_other_subscription_products = false;
|
||||
if ( empty( WC()->cart->cart_contents ) || ! WC_Subscriptions_Product::is_subscription( $product_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! empty( WC()->cart->cart_contents ) && WC_Subscriptions_Product::is_subscription( $product_id ) ) {
|
||||
$is_subscription = WC_Subscriptions_Product::is_subscription( $product_id );
|
||||
foreach ( WC()->cart->cart_contents as $cart_item ) {
|
||||
if ( wcs_get_canonical_product_id( $cart_item ) !== $product_id ) {
|
||||
$cart_contains_other_subscription_products = true;
|
||||
break;
|
||||
}
|
||||
foreach ( WC()->cart->cart_contents as $cart_item ) {
|
||||
$item_product_id = wcs_get_canonical_product_id( $cart_item );
|
||||
$is_subscription = isset( $item_product_id ) ? WC_Subscriptions_Product::is_subscription( $item_product_id ) : false;
|
||||
if ( $item_product_id !== $product_id && $is_subscription ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return $cart_contains_other_subscription_products;
|
||||
// Return false because no other subscription product was found in the cart.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1485,7 +1517,7 @@ class WC_Subscriptions_Cart {
|
|||
*/
|
||||
public static function pre_get_refreshed_fragments() {
|
||||
wcs_deprecated_function( __METHOD__, '2.5.0' );
|
||||
if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) {
|
||||
if ( wp_doing_ajax() && ! defined( 'WOOCOMMERCE_CART' ) ) {
|
||||
define( 'WOOCOMMERCE_CART', true );
|
||||
WC()->cart->calculate_totals();
|
||||
}
|
||||
|
|
@ -2538,4 +2570,17 @@ class WC_Subscriptions_Cart {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the item names from the shipping rate
|
||||
* to check if the rates match.
|
||||
*
|
||||
* @param WC_Shipping_Rate $rate The rate.
|
||||
* @return WC_Shipping_Rate The rate.
|
||||
*/
|
||||
private static function remove_item_names_from_rate( $rate ) {
|
||||
$rate->add_meta_data( 'Items', null );
|
||||
|
||||
return $rate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,11 @@ class WC_Subscriptions_Change_Payment_Gateway {
|
|||
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
|
||||
$subscription_key = isset( $_GET['key'] ) ? wc_clean( $_GET['key'] ) : '';
|
||||
|
||||
if ( ! $subscription ) {
|
||||
wc_print_notice( __( 'There was an unexpected problem with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'before_woocommerce_pay' );
|
||||
|
||||
/**
|
||||
|
|
@ -276,19 +281,27 @@ class WC_Subscriptions_Change_Payment_Gateway {
|
|||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v1.4
|
||||
*/
|
||||
public static function change_payment_method_via_pay_shortcode() {
|
||||
|
||||
if ( ! isset( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_POST['_wcsnonce'] ) ), 'wcs_change_payment_method' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription_id = absint( wc_clean( wp_unslash( $_POST['woocommerce_change_payment'] ) ) );
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
wc_add_notice( __( 'There was an unexpected problem with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription );
|
||||
|
||||
ob_start();
|
||||
if ( ! $subscription instanceof WC_Subscription || $subscription->get_order_key() !== wc_clean( wp_unslash( $_GET['key'] ?? '' ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $subscription->get_order_key() == wc_clean( wp_unslash( $_GET['key'] ) ) ) {
|
||||
try {
|
||||
// We open an output buffer to suppress any error noise that might break the redirect.
|
||||
$output_buffer_opened = ob_start();
|
||||
|
||||
$subscription_billing_country = $subscription->get_billing_country();
|
||||
$subscription_billing_state = $subscription->get_billing_state();
|
||||
|
|
@ -359,6 +372,11 @@ class WC_Subscriptions_Change_Payment_Gateway {
|
|||
*/
|
||||
$subscription = wcs_get_subscription( $subscription->get_id() );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
wc_add_notice( __( 'There was an unexpected problem with your request. Please try again.', 'woocommerce-subscriptions' ), 'error' );
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription->set_requires_manual_renewal( false );
|
||||
$subscription->save();
|
||||
|
||||
|
|
@ -383,9 +401,13 @@ class WC_Subscriptions_Change_Payment_Gateway {
|
|||
wp_safe_redirect( $result['redirect'] );
|
||||
exit;
|
||||
}
|
||||
} finally {
|
||||
// Close the output buffer (if we exited, this will have happened automatically, so this is primarily here
|
||||
// to ensure we clean-up in the event that we returned early).
|
||||
if ( $output_buffer_opened ) {
|
||||
ob_clean();
|
||||
}
|
||||
}
|
||||
|
||||
ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -595,7 +617,12 @@ class WC_Subscriptions_Change_Payment_Gateway {
|
|||
*/
|
||||
if ( ! $is_change_payment_method_request && $cart_contains_failed_renewal ) {
|
||||
$subscription = wcs_get_subscription( $renewal_order_cart_item['subscription_renewal']['subscription_id'] );
|
||||
$cart_contains_failed_manual_renewal = $subscription->is_manual();
|
||||
|
||||
// If the subscription has been deleted (which is a reason $subscription may be false), we probably do not
|
||||
// need to concern ourselves with a failed manual renewal.
|
||||
if ( $subscription ) {
|
||||
$cart_contains_failed_manual_renewal = $subscription->is_manual();
|
||||
}
|
||||
}
|
||||
|
||||
if ( apply_filters( 'wcs_payment_gateways_change_payment_method', $is_change_payment_method_request || ( $cart_contains_failed_renewal && ! $cart_contains_failed_manual_renewal ) ) ) {
|
||||
|
|
@ -728,6 +755,7 @@ class WC_Subscriptions_Change_Payment_Gateway {
|
|||
}
|
||||
|
||||
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return $title;
|
||||
}
|
||||
|
|
@ -823,19 +851,19 @@ class WC_Subscriptions_Change_Payment_Gateway {
|
|||
|
||||
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
|
||||
|
||||
if ( $subscription ) {
|
||||
wc_add_notice( __( 'Please log in to your account below to choose a new payment method for your subscription.', 'woocommerce-subscriptions' ), 'notice' );
|
||||
|
||||
ob_start();
|
||||
woocommerce_login_form( array(
|
||||
'redirect' => $subscription->get_change_payment_method_url(),
|
||||
'message' => wc_print_notices( true ),
|
||||
) );
|
||||
|
||||
$content = ob_get_clean();
|
||||
if ( ! $subscription ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
return $content;
|
||||
wc_add_notice( __( 'Please log in to your account below to choose a new payment method for your subscription.', 'woocommerce-subscriptions' ), 'notice' );
|
||||
|
||||
ob_start();
|
||||
woocommerce_login_form( array(
|
||||
'redirect' => $subscription->get_change_payment_method_url(),
|
||||
'message' => wc_print_notices( true ),
|
||||
) );
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/** Deprecated Functions **/
|
||||
|
|
|
|||
|
|
@ -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 "Place order" button text for subscription carts.
|
||||
*
|
||||
* @since 7.8.0
|
||||
* @param string $button_text The "Place order" button text.
|
||||
* @return string The "Place order" button text.
|
||||
*/
|
||||
return apply_filters( 'wcs_place_subscription_order_text', __( 'Place order', 'woocommerce-subscriptions' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -542,6 +542,9 @@ class WC_Subscriptions_Core_Plugin {
|
|||
update_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$switch_setting_string, 'yes' );
|
||||
}
|
||||
|
||||
// Setup downloads table.
|
||||
WC_Subscription_Downloads::install();
|
||||
|
||||
update_option( WC_Subscriptions_Admin::$option_prefix . '_is_active', true );
|
||||
|
||||
set_transient( $this->get_activation_transient(), true, 60 * 60 );
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ class WC_Subscriptions_Coupon {
|
|||
// Add our recurring product coupon types to the list of coupon types that apply to individual products
|
||||
add_filter( 'woocommerce_product_coupon_types', __CLASS__ . '::filter_product_coupon_types', 10, 1 );
|
||||
|
||||
if ( ! is_admin() ) {
|
||||
// WC 3.0 only sets a coupon type if it is a pre-defined supported type, so we need to temporarily add our pseudo types. We don't want to add these on admin pages.
|
||||
// Adds pseudo coupons on every page so orders can be created and renewed with them but prevent showing them on the coupon edit page.
|
||||
if ( ! self::is_coupon_edit_page() ) {
|
||||
add_filter( 'woocommerce_coupon_discount_types', __CLASS__ . '::add_pseudo_coupon_types' );
|
||||
}
|
||||
|
||||
|
|
@ -748,6 +748,29 @@ class WC_Subscriptions_Coupon {
|
|||
return isset( self::$recurring_coupons[ $coupon_type ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current page is the coupon edit page.
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v4.0.0
|
||||
*
|
||||
* @return bool Whether the current page is the coupon edit page.
|
||||
*/
|
||||
public static function is_coupon_edit_page() {
|
||||
global $pagenow;
|
||||
|
||||
$current_post_type = '';
|
||||
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
if ( 'post.php' === $pagenow && isset( $_GET['post'] ) ) {
|
||||
$current_post_type = get_post_type( $_GET['post'] );
|
||||
} elseif ( isset( $_GET['post_type'] ) ) {
|
||||
$current_post_type = $_GET['post_type'];
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
|
||||
return 'shop_coupon' === $current_post_type;
|
||||
}
|
||||
|
||||
/* Deprecated */
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -156,6 +156,11 @@ class WC_Subscriptions_Email_Notifications {
|
|||
switch ( current_action() ) {
|
||||
case 'woocommerce_scheduled_subscription_customer_notification_renewal':
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $subscription->get_total() <= 0 ) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -167,6 +172,11 @@ class WC_Subscriptions_Email_Notifications {
|
|||
break;
|
||||
case 'woocommerce_scheduled_subscription_customer_notification_trial_expiration':
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $subscription->is_manual() ) {
|
||||
$notification = $emails['WCS_Email_Customer_Notification_Manual_Trial_Expiration'];
|
||||
} else {
|
||||
|
|
@ -268,7 +278,7 @@ class WC_Subscriptions_Email_Notifications {
|
|||
|
||||
$notification_settings = [
|
||||
[
|
||||
'name' => __( 'Customer Notifications', 'woocommerce-subscriptions' ),
|
||||
'name' => __( 'Customer notifications', 'woocommerce-subscriptions' ),
|
||||
'type' => 'title',
|
||||
'id' => WC_Subscriptions_Admin::$option_prefix . '_customer_notifications',
|
||||
/* translators: Link to WC Settings > Email. */
|
||||
|
|
@ -304,7 +314,9 @@ class WC_Subscriptions_Email_Notifications {
|
|||
],
|
||||
];
|
||||
|
||||
WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_miscellaneous', $notification_settings, 'multiple_settings', 'sectionend' );
|
||||
if ( ! WC_Subscriptions_Admin::insert_setting_after( $settings, WC_Subscriptions_Admin::$option_prefix . '_miscellaneous', $notification_settings, 'multiple_settings', 'sectionend' ) ) {
|
||||
$settings = array_merge( $settings, $notification_settings );
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ class WC_Subscriptions_Extend_Store_Endpoint {
|
|||
// @phpstan-ignore class.notFound
|
||||
self::$currency_formatter = function_exists( 'woocommerce_store_api_get_formatter' ) ? woocommerce_store_api_get_formatter( 'currency' ) : Package::container()->get( ExtendRestApi::class )->get_formatter( 'currency' );
|
||||
self::extend_store();
|
||||
|
||||
add_action( 'woocommerce_store_api_cart_select_shipping_rate', [ __CLASS__, 'initial_shipment_select_shipping_rate' ], 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -273,9 +275,10 @@ class WC_Subscriptions_Extend_Store_Endpoint {
|
|||
// Add extra package data to array.
|
||||
if ( count( $packages ) ) {
|
||||
$packages = array_map(
|
||||
function( $key, $package, $index ) use ( $cart ) {
|
||||
function ( $key, $package, $index ) use ( $cart ) {
|
||||
$package['package_id'] = isset( $package['package_id'] ) ? $package['package_id'] : $key;
|
||||
$package['package_name'] = isset( $package['package_name'] ) ? $package['package_name'] : self::get_shipping_package_name( $package, $cart );
|
||||
$package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package );
|
||||
return $package;
|
||||
},
|
||||
array_keys( $packages ),
|
||||
|
|
@ -331,6 +334,7 @@ class WC_Subscriptions_Extend_Store_Endpoint {
|
|||
}
|
||||
|
||||
$future_subscriptions = array();
|
||||
$standard_packages = WC()->shipping->get_packages();
|
||||
|
||||
if ( ! empty( wc()->cart->recurring_carts ) ) {
|
||||
foreach ( wc()->cart->recurring_carts as $cart_key => $cart ) {
|
||||
|
|
@ -359,7 +363,18 @@ class WC_Subscriptions_Extend_Store_Endpoint {
|
|||
'tax_lines' => self::get_tax_lines( $cart ),
|
||||
)
|
||||
),
|
||||
'shipping_rates' => array_values( array_map( array( self::$schema->get( 'cart-shipping-rate' ), 'get_item_response' ), $shipping_packages ) ),
|
||||
'shipping_rates' => array_values(
|
||||
array_map(
|
||||
function ( $package ) use ( $cart, $cart_key, $standard_packages ) {
|
||||
$shipping_package = self::$schema->get( 'cart-shipping-rate' )->get_item_response( $package );
|
||||
$shipping_package['match_initial_rates'] = WC_Subscriptions_Cart::package_rates_match_initial_rates( $standard_packages, $package, $cart_key, $cart );
|
||||
$shipping_package['needs_shipping'] = WC_Subscriptions_Cart::cart_contains_subscriptions_needing_shipping( $cart );
|
||||
|
||||
return $shipping_package;
|
||||
},
|
||||
$shipping_packages
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -367,6 +382,42 @@ class WC_Subscriptions_Extend_Store_Endpoint {
|
|||
return $future_subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the initial shipment shipping rate.
|
||||
*
|
||||
* @param string $package_id Package ID.
|
||||
* @param string $rate_id Rate ID.
|
||||
*/
|
||||
public static function initial_shipment_select_shipping_rate( $package_id, $rate_id ) {
|
||||
WC()->cart->calculate_totals();
|
||||
|
||||
$standard_packages = WC()->shipping->get_packages();
|
||||
|
||||
if ( ! is_numeric( $package_id ) || key( $standard_packages ) !== (int) $package_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) {
|
||||
if ( false === $recurring_cart->needs_shipping() || 0 === $recurring_cart->next_payment_date ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WC_Subscriptions_Cart::set_calculation_type( 'recurring_total' );
|
||||
WC_Subscriptions_Cart::set_recurring_cart_key( $recurring_cart_key );
|
||||
WC_Subscriptions_Cart::set_cached_recurring_cart( $recurring_cart );
|
||||
|
||||
foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_key => $recurring_cart_package ) {
|
||||
if ( WC_Subscriptions_Cart::package_rates_match_initial_rates( $standard_packages, $recurring_cart_package, $recurring_cart_key, $recurring_cart ) ) {
|
||||
$session_data = wc()->session->get( 'chosen_shipping_methods' ) ? wc()->session->get( 'chosen_shipping_methods' ) : [];
|
||||
|
||||
$session_data[ $recurring_cart_package_key ] = $rate_id;
|
||||
|
||||
wc()->session->set( 'chosen_shipping_methods', $session_data );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format sign-up fees.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ class WC_Subscriptions_Frontend_Scripts {
|
|||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v3.1.3
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
global $post;
|
||||
|
||||
$dependencies = array( 'jquery' );
|
||||
|
||||
if ( is_cart() || is_checkout() ) {
|
||||
wp_enqueue_script( 'wcs-cart', self::get_file_url( 'assets/js/frontend/wcs-cart.js' ), $dependencies, WC_Subscriptions_Core_Plugin::instance()->get_library_version(), true );
|
||||
} elseif ( is_product() ) {
|
||||
} elseif ( is_product() && WC_Subscriptions_Product::is_subscription( $post->ID ) ) {
|
||||
wp_enqueue_script( 'wcs-single-product', self::get_file_url( 'assets/js/frontend/single-product.js' ), $dependencies, WC_Subscriptions_Core_Plugin::instance()->get_library_version(), true );
|
||||
} elseif ( wcs_is_view_subscription_page() ) {
|
||||
$subscription = wcs_get_subscription( absint( get_query_var( 'view-subscription' ) ) );
|
||||
|
|
@ -66,6 +68,7 @@ class WC_Subscriptions_Frontend_Scripts {
|
|||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v3.1.3
|
||||
*/
|
||||
public static function enqueue_styles( $styles ) {
|
||||
global $post;
|
||||
|
||||
if ( is_checkout() || is_cart() ) {
|
||||
$styles['wcs-checkout'] = array(
|
||||
|
|
@ -83,6 +86,34 @@ class WC_Subscriptions_Frontend_Scripts {
|
|||
);
|
||||
}
|
||||
|
||||
if (
|
||||
wp_is_block_theme() ||
|
||||
(
|
||||
! empty( $post ) &&
|
||||
(
|
||||
WC_Blocks_Utils::has_block_in_page( $post->ID, 'woocommerce/cart' ) ||
|
||||
WC_Blocks_Utils::has_block_in_page( $post->ID, 'woocommerce/mini-cart' ) ||
|
||||
WC_Blocks_Utils::has_block_in_page( $post->ID, 'woocommerce/checkout' )
|
||||
)
|
||||
)
|
||||
) {
|
||||
$styles['wcs-blocks-integration'] = array(
|
||||
'src' => \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( 'build/index.css' ),
|
||||
'deps' => 'wc-checkout',
|
||||
'version' => WCS_Blocks_Integration::get_file_version( \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/index.css' ) ),
|
||||
'media' => 'all',
|
||||
);
|
||||
|
||||
if ( WCSG_Admin::is_gifting_enabled() ) {
|
||||
$styles['wcsg-blocks-integration'] = array(
|
||||
'src' => \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( 'build/wcsg-blocks-integration.css' ),
|
||||
'deps' => 'wc-checkout',
|
||||
'version' => WCS_Blocks_Integration::get_file_version( \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/wcsg-blocks-integration.css' ) ),
|
||||
'media' => 'all',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $styles;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,11 +130,10 @@ class WC_Subscriptions_Manager {
|
|||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.12
|
||||
*/
|
||||
public static function process_renewal( $subscription_id, $required_status, $order_note ) {
|
||||
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
// If the subscription is using manual payments, the gateway isn't active or it manages scheduled payments
|
||||
if ( ! empty( $subscription ) && $subscription->has_status( $required_status ) && ( 0 == $subscription->get_total() || $subscription->is_manual() || '' == $subscription->get_payment_method() || ! $subscription->payment_method_supports( 'gateway_scheduled_payments' ) ) ) {
|
||||
if ( $subscription instanceof WC_Subscription && $subscription->has_status( $required_status ) && ( 0 === absint( $subscription->get_total() ) || $subscription->is_manual() || '' === $subscription->get_payment_method() || ! $subscription->payment_method_supports( 'gateway_scheduled_payments' ) ) ) {
|
||||
|
||||
// Always put the subscription on hold in case something goes wrong while trying to process renewal
|
||||
$subscription->update_status( 'on-hold', $order_note );
|
||||
|
|
@ -438,11 +437,13 @@ class WC_Subscriptions_Manager {
|
|||
/**
|
||||
* Called when a sign up fails during the payment processing step.
|
||||
*
|
||||
* This method only performs actions when a parent order changed to failed status.
|
||||
* Overlaps with WC_Subscriptions_Order::maybe_record_subscription_payment.
|
||||
*
|
||||
* @param WC_Order|int $order The order or ID of the order for which subscriptions should be marked as failed.
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v1.0
|
||||
*/
|
||||
public static function failed_subscription_sign_ups_for_order( $order ) {
|
||||
|
||||
$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'parent' ) );
|
||||
|
||||
if ( empty( $subscriptions ) ) {
|
||||
|
|
@ -461,9 +462,23 @@ class WC_Subscriptions_Manager {
|
|||
foreach ( $subscriptions as $subscription ) {
|
||||
|
||||
try {
|
||||
$new_status = $subscription->has_status( 'pending' ) && $subscription->get_parent_id() === $order->get_id() ? 'pending' : 'on-hold';
|
||||
$subscription->payment_failed( $new_status );
|
||||
/**
|
||||
* Parent status failure should not change the subscription status in most cases.
|
||||
* We only change the status when the parent order is the last order and subscription is active.
|
||||
* In that case, we need to put the subscription on hold as we do when the latest renewal order fails.
|
||||
* Examples:
|
||||
* - if subscription is pending and initial payment fails, we keep subscription as pending to activate it on successful payment.
|
||||
* - if subscription has successful renewals and somehow the parent order fails after that, we still keep subscription as active.
|
||||
* - if subscription is active after successful initial payment, but later the parent order fails, we move subscription to on-hold.
|
||||
*/
|
||||
$new_status = $subscription->get_status();
|
||||
|
||||
if ( 'active' === $new_status && $subscription->get_parent_id() === $order->get_id() ) {
|
||||
$last_order = $subscription->get_last_order( 'ids', 'any' );
|
||||
$new_status = $last_order === $order->get_id() ? 'on-hold' : 'active';
|
||||
}
|
||||
|
||||
$subscription->payment_failed_for_related_order( $new_status, $order );
|
||||
} catch ( Exception $e ) {
|
||||
// translators: $1: order number, $2: error message
|
||||
$subscription->add_order_note( sprintf( __( 'Failed to process failed payment on subscription for order #%1$s: %2$s', 'woocommerce-subscriptions' ), is_object( $order ) ? $order->get_order_number() : $order, $e->getMessage() ) );
|
||||
|
|
@ -1020,6 +1035,24 @@ class WC_Subscriptions_Manager {
|
|||
foreach ( $subscriptions as $subscription ) {
|
||||
$subscription_number = $subscription->get_order_number();
|
||||
|
||||
if ( WCS_Gifting::is_gifted_subscription( $subscription ) ) {
|
||||
// translators: %s is the email address of the recipient.
|
||||
$message = sprintf( __( 'The gifted subscription moved to the purchaser after the recipient user with email address %s was deleted.', 'woocommerce-subscriptions' ), $subscription->get_meta( '_recipient_user_email_address' ) );
|
||||
|
||||
$subscription->delete_meta_data( '_recipient_user' );
|
||||
$subscription->delete_meta_data( '_recipient_user_email_address' );
|
||||
$subscription->save();
|
||||
|
||||
$subscription->add_order_note( $message, $subscription_number );
|
||||
|
||||
// Check needed for tests.
|
||||
if ( $subscription->get_parent() ) {
|
||||
$subscription->get_parent()->add_order_note( $message, $subscription_number );
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Before deleting the subscription, add an order note to the related orders.
|
||||
foreach ( $subscription->get_related_orders( 'all', array( 'parent', 'renewal', 'switch' ) ) as $order ) {
|
||||
if ( $current_user ) {
|
||||
|
|
@ -2041,6 +2074,11 @@ class WC_Subscriptions_Manager {
|
|||
*/
|
||||
public static function maybe_process_failed_renewal_for_repair( $subscription_id ) {
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'true' === $subscription->get_meta( '_wcs_repaired_2_0_2_needs_failed_payment', true ) ) {
|
||||
// Always put the subscription on hold in case something goes wrong while trying to process renewal
|
||||
$subscription->update_status( 'on-hold', _x( 'Subscription renewal payment due:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) );
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -484,7 +485,7 @@ class WC_Subscriptions_Order {
|
|||
* @param string $new_order_status The new order status.
|
||||
*/
|
||||
public static function maybe_record_subscription_payment( $order_id, $old_order_status, $new_order_status ) {
|
||||
|
||||
// ! This hook will only perform actions on parent order status changes.
|
||||
if ( ! wcs_order_contains_subscription( $order_id, 'parent' ) ) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -577,10 +578,6 @@ class WC_Subscriptions_Order {
|
|||
$subscription->payment_complete_for_order( $order );
|
||||
$was_activated = true;
|
||||
|
||||
} elseif ( 'failed' === $new_order_status ) {
|
||||
// When parent order fails, we want to keep the subscription status as pending unless it had another status before.
|
||||
$new_status = $subscription->has_status( 'pending' ) && $subscription->get_parent_id() === $order->get_id() ? 'pending' : 'on-hold';
|
||||
$subscription->payment_failed( $new_status );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -589,6 +586,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 */
|
||||
|
||||
/**
|
||||
|
|
@ -2451,7 +2478,6 @@ f *
|
|||
|
||||
if ( isset( $order->$meta_key ) ) { // WC 2.1+ magic __isset() & __get() methods
|
||||
$meta_value = $order->$meta_key;
|
||||
// @phpstan-ignore-next-line
|
||||
} elseif ( is_array( $order->order_custom_fields ) && isset( $order->order_custom_fields[ '_' . $meta_key ][0] ) && $order->order_custom_fields[ '_' . $meta_key ][0] ) { // < WC 2.1+
|
||||
$meta_value = maybe_unserialize( $order->order_custom_fields[ '_' . $meta_key ][0] );
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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,24 +1275,12 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
|
||||
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_purchasable( $is_purchasable, $product ) {
|
||||
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_product' );
|
||||
return WCS_Limiter::is_purchasable_product( $is_purchasable, $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current session has an order awaiting payment for a subscription to a specific product line item.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -81,10 +81,12 @@ class WC_Subscriptions_Renewal_Order {
|
|||
* subscriptions are updated even if payment is processed by a manual payment gateways (which would never trigger the
|
||||
* 'woocommerce_payment_complete' hook) or by some other means that circumvents that hook.
|
||||
*
|
||||
* This hook will be skipped for early renewal orders transitioning to statuses other than cancelled or refunded.
|
||||
* @see WCS_Cart_Early_Renewal::maybe_record_subscription_payment().
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
|
||||
*/
|
||||
public static function maybe_record_subscription_payment( $order_id, $orders_old_status, $orders_new_status ) {
|
||||
|
||||
if ( ! wcs_order_contains_renewal( $order_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ class WC_Subscriptions_Synchroniser {
|
|||
public static function add_settings( $settings ) {
|
||||
$synchronisation_settings = array(
|
||||
array(
|
||||
'name' => __( 'Synchronisation', 'woocommerce-subscriptions' ),
|
||||
'name' => __( 'Synchronization', 'woocommerce-subscriptions' ),
|
||||
'type' => 'title',
|
||||
// translators: placeholders are opening and closing link tags
|
||||
'desc' => sprintf( _x( 'Align subscription renewal to a specific day of the week, month or year. For example, the first day of the month. %1$sLearn more%2$s.', 'used in the general subscription options page', 'woocommerce-subscriptions' ), '<a href="' . esc_url( 'https://woocommerce.com/document/subscriptions/renewal-synchronisation/' ) . '">', '</a>' ),
|
||||
|
|
@ -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 ) {
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,17 @@
|
|||
* @class WC_Subscriptions_Tracker
|
||||
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.6.4
|
||||
* @package WooCommerce Subscriptions/Classes
|
||||
* @category Class
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce_Subscriptions\Internal\Telemetry\Collector;
|
||||
|
||||
class WC_Subscriptions_Tracker {
|
||||
/**
|
||||
* Handles the collection of additional pieces of data.
|
||||
*/
|
||||
private static Collector $telemetry_collector;
|
||||
|
||||
/**
|
||||
* Initialize the Tracker.
|
||||
|
|
@ -18,6 +23,8 @@ class WC_Subscriptions_Tracker {
|
|||
public static function init() {
|
||||
// Only add data if Tracker enabled
|
||||
if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) {
|
||||
self::$telemetry_collector = new Collector();
|
||||
self::$telemetry_collector->setup();
|
||||
add_filter( 'woocommerce_tracker_data', [ __CLASS__, 'add_subscriptions_tracking_data' ], 10, 1 );
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +39,11 @@ class WC_Subscriptions_Tracker {
|
|||
$data['extensions']['wc_subscriptions']['settings'] = self::get_subscriptions_options();
|
||||
$data['extensions']['wc_subscriptions']['subscriptions'] = self::get_subscriptions();
|
||||
$data['extensions']['wc_subscriptions']['subscription_orders'] = self::get_subscription_orders();
|
||||
|
||||
// Insert any additional telemetry that we have been collecting.
|
||||
$additional_telemetry = self::$telemetry_collector->get_telemetry_data();
|
||||
$data['extensions']['wc_subscriptions'] = array_merge_recursive( $data['extensions']['wc_subscriptions'], $additional_telemetry );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
|
@ -41,47 +53,86 @@ class WC_Subscriptions_Tracker {
|
|||
* @return array Subscriptions options data.
|
||||
*/
|
||||
private static function get_subscriptions_options() {
|
||||
$customer_notifications_offset = get_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$offset_setting_string, [] );
|
||||
|
||||
return [
|
||||
// Staging and live site
|
||||
'wc_subscriptions_staging' => WCS_Staging::is_duplicate_site() ? 'staging' : 'live',
|
||||
'wc_subscriptions_live_url' => esc_url( WCS_Staging::get_site_url_from_source( 'subscriptions_install' ) ),
|
||||
|
||||
// Button text, roles
|
||||
// Button text
|
||||
// Add to Cart Button Text
|
||||
'add_to_cart_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_add_to_cart_button_text' ),
|
||||
// Place Order Button Text
|
||||
'order_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_order_button_text' ),
|
||||
|
||||
// Roles
|
||||
// Subscriber Default Role
|
||||
'subscriber_role' => get_option( WC_Subscriptions_Admin::$option_prefix . '_subscriber_role' ),
|
||||
// Inactive Subscriber Role
|
||||
'cancelled_role' => get_option( WC_Subscriptions_Admin::$option_prefix . '_cancelled_role' ),
|
||||
|
||||
// Renewals
|
||||
// Accept Manual Renewals
|
||||
'accept_manual_renewals' => get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals' ),
|
||||
// Turn off Automatic Payments
|
||||
'turn_off_automatic_payments' => 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'none' ),
|
||||
// Auto Renewal Toggle
|
||||
'enable_auto_renewal_toggle' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_auto_renewal_toggle' ),
|
||||
|
||||
// Early renewal
|
||||
// Accept Early Renewal Payments
|
||||
'enable_early_renewal' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal' ),
|
||||
// Accept Early Renewal Payments via a Modal
|
||||
'enable_early_renewal_via_modal' => 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_early_renewal_via_modal', 'none' ),
|
||||
|
||||
// Switching
|
||||
// Between Subscription Variations and Between Grouped Subscriptions are condensed into this setting.
|
||||
'allow_switching' => get_option( WC_Subscriptions_Admin::$option_prefix . '_allow_switching' ),
|
||||
// Prorate Recurring Payment
|
||||
'apportion_recurring_price' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_recurring_price', 'none' ),
|
||||
// Prorate Sign up Fee
|
||||
'apportion_sign_up_fee' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_sign_up_fee', 'none' ),
|
||||
// Prorate Subscription Length
|
||||
'apportion_length' => get_option( WC_Subscriptions_Admin::$option_prefix . '_apportion_length', 'none' ),
|
||||
// Switch Button Text
|
||||
'switch_button_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_switch_button_text', 'none' ),
|
||||
|
||||
// Gifting
|
||||
// Enable gifting
|
||||
'gifting_enable_gifting' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_enable_gifting' ),
|
||||
'gifting_default_option' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_default_option' ),
|
||||
// Gifting Checkbox Text
|
||||
'gifting_gifting_checkbox_text' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_gifting_checkbox_text' ),
|
||||
// Downloadable Products
|
||||
'gifting_downloadable_products' => get_option( WC_Subscriptions_Admin::$option_prefix . '_gifting_downloadable_products' ),
|
||||
|
||||
// Synchronization
|
||||
// Synchronise renewals
|
||||
'sync_payments' => get_option( WC_Subscriptions_Admin::$option_prefix . '_sync_payments' ),
|
||||
// Prorate First Renewal
|
||||
'prorate_synced_payments' => $prorate_synced_payments = ( 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_sync_payments' ) ? 'none' : get_option( WC_Subscriptions_Admin::$option_prefix . '_prorate_synced_payments', 'none' ) ),
|
||||
// Sign-up grace period
|
||||
'days_no_fee' => 'recurring' === $prorate_synced_payments ? get_option( WC_Subscriptions_Admin::$option_prefix . '_days_no_fee', 'none' ) : 'none',
|
||||
|
||||
// Miscellaneous
|
||||
// Customer Suspensions
|
||||
'max_customer_suspensions' => get_option( WC_Subscriptions_Admin::$option_prefix . '_max_customer_suspensions' ),
|
||||
// Mixed Checkout
|
||||
'multiple_purchase' => get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase' ),
|
||||
// $0 Initial Checkout
|
||||
'allow_zero_initial_order_without_payment_method' => get_option( WC_Subscriptions_Admin::$option_prefix . '_zero_initial_payment_requires_payment' ),
|
||||
// Drip Downloadable Content
|
||||
'drip_downloadable_content_on_renewal' => get_option( WC_Subscriptions_Admin::$option_prefix . '_drip_downloadable_content_on_renewal' ),
|
||||
// Retry Failed Payments
|
||||
'enable_retry' => get_option( WC_Subscriptions_Admin::$option_prefix . '_enable_retry' ),
|
||||
|
||||
// Notifications
|
||||
'enable_notification_reminders' => get_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$switch_setting_string, 'no' ),
|
||||
// Enable Reminders
|
||||
'enable_notification_reminders' => get_option( WC_Subscriptions_Admin::$option_prefix . WC_Subscriptions_Email_Notifications::$switch_setting_string ),
|
||||
// Reminder Timing
|
||||
'customer_notifications_offset_number' => $customer_notifications_offset['number'] ?? 'none',
|
||||
'customer_notifications_offset_unit' => $customer_notifications_offset['unit'] ?? 'none',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -119,80 +170,21 @@ class WC_Subscriptions_Tracker {
|
|||
* @return array Subscription order counts and totals by type (initial, switch, renewal, resubscribe). Values are returned as strings.
|
||||
*/
|
||||
private static function get_subscription_orders() {
|
||||
$order_totals = [];
|
||||
$relation_types = [
|
||||
'switch',
|
||||
'renewal',
|
||||
'resubscribe',
|
||||
];
|
||||
$order_totals = array();
|
||||
|
||||
// Get the subtotal and count for each subscription type.
|
||||
foreach ( $relation_types as $relation_type ) {
|
||||
// Get orders with the given relation type.
|
||||
$relation_orders = wcs_get_orders_with_meta_query(
|
||||
[
|
||||
'type' => 'shop_order',
|
||||
'status' => [ 'wc-completed', 'wc-processing', 'wc-refunded' ],
|
||||
'limit' => -1,
|
||||
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
[
|
||||
'key' => '_subscription_' . $relation_type,
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
// Get the subtotal and count for all subscription types in one query
|
||||
$counts_and_totals = self::get_order_count_and_total_by_meta_key();
|
||||
|
||||
// Sum the totals and count the orders.
|
||||
$count = count( $relation_orders );
|
||||
$total = array_reduce(
|
||||
$relation_orders,
|
||||
function( $total, $order ) {
|
||||
return $total + $order->get_total();
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
$order_totals[ $relation_type . '_gross' ] = $total;
|
||||
$order_totals[ $relation_type . '_count' ] = $count;
|
||||
foreach ( $counts_and_totals as $type => $data ) {
|
||||
$order_totals[ $type . '_gross' ] = $data['total'];
|
||||
$order_totals[ $type . '_count' ] = $data['count'];
|
||||
}
|
||||
|
||||
// Finally, get the initial revenue and count.
|
||||
// Get the orders for all initial subscription orders (no switch, renewal or resubscribe meta key).
|
||||
$initial_subscription_orders = wcs_get_orders_with_meta_query(
|
||||
[
|
||||
'type' => 'shop_order',
|
||||
'status' => [ 'wc-completed', 'wc-processing', 'wc-refunded' ],
|
||||
'limit' => -1,
|
||||
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
[
|
||||
'key' => '_subscription_switch',
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
[
|
||||
'key' => '_subscription_renewal',
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
[
|
||||
'key' => '_subscription_resubscribe',
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
// Get initial orders (orders without switch, renewal, or resubscribe meta keys).
|
||||
$count_and_total = self::get_initial_order_count_and_total();
|
||||
|
||||
// Sum the totals and count the orders.
|
||||
$initial_order_count = count( $initial_subscription_orders );
|
||||
$initial_order_total = array_reduce(
|
||||
$initial_subscription_orders,
|
||||
function( $total, $order ) {
|
||||
return $total + $order->get_total();
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
$order_totals['initial_gross'] = $initial_order_total;
|
||||
$order_totals['initial_count'] = $initial_order_count;
|
||||
$order_totals['initial_gross'] = $count_and_total['total'];
|
||||
$order_totals['initial_count'] = $count_and_total['count'];
|
||||
|
||||
// Ensure all values are strings.
|
||||
$order_totals = array_map( 'strval', $order_totals );
|
||||
|
|
@ -200,6 +192,149 @@ class WC_Subscriptions_Tracker {
|
|||
return $order_totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets order count and total for subscription-related orders.
|
||||
*
|
||||
* @return array Array with counts and totals for switch, renewal, and resubscribe orders.
|
||||
*/
|
||||
private static function get_order_count_and_total_by_meta_key() {
|
||||
global $wpdb;
|
||||
|
||||
$order_statuses = array( 'wc-completed', 'wc-refunded' );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
// HPOS: Use wc_orders and wc_orders_meta tables.
|
||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||
$meta_table = $wpdb->prefix . 'wc_orders_meta';
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
order_relation.meta_key as 'type',
|
||||
SUM( orders.total_amount ) AS 'total',
|
||||
COUNT( orders.id ) as 'count'
|
||||
FROM {$orders_table} AS orders
|
||||
RIGHT JOIN {$meta_table} AS order_relation ON order_relation.order_id = orders.id
|
||||
WHERE order_relation.meta_key IN ( '_subscription_switch', '_subscription_renewal', '_subscription_resubscribe' )
|
||||
AND orders.status in (%s, %s)
|
||||
AND orders.type = 'shop_order'
|
||||
GROUP BY order_relation.meta_key",
|
||||
$order_statuses
|
||||
);
|
||||
} else {
|
||||
// CPT: Use posts and postmeta tables.
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
order_relation.meta_key as 'type',
|
||||
SUM( order_total.meta_value ) AS 'total',
|
||||
COUNT( orders.ID ) as 'count'
|
||||
FROM {$wpdb->prefix}posts AS orders
|
||||
RIGHT JOIN {$wpdb->prefix}postmeta AS order_relation ON order_relation.post_id = orders.ID
|
||||
RIGHT JOIN {$wpdb->prefix}postmeta AS order_total ON order_total.post_id = orders.ID
|
||||
WHERE order_relation.meta_key IN ( '_subscription_switch', '_subscription_renewal', '_subscription_resubscribe' )
|
||||
AND orders.post_status in (%s, %s)
|
||||
AND order_total.meta_key = '_order_total'
|
||||
GROUP BY order_relation.meta_key",
|
||||
$order_statuses
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
|
||||
$results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
$totals = array(
|
||||
'switch' => array(
|
||||
'count' => 0,
|
||||
'total' => 0.0,
|
||||
),
|
||||
'renewal' => array(
|
||||
'count' => 0,
|
||||
'total' => 0.0,
|
||||
),
|
||||
'resubscribe' => array(
|
||||
'count' => 0,
|
||||
'total' => 0.0,
|
||||
),
|
||||
);
|
||||
|
||||
if ( $results ) {
|
||||
foreach ( $results as $result ) {
|
||||
$type = str_replace( '_subscription_', '', $result['type'] );
|
||||
if ( isset( $totals[ $type ] ) ) {
|
||||
$totals[ $type ] = array(
|
||||
'count' => (int) $result['count'],
|
||||
'total' => (float) $result['total'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log if any type has no data
|
||||
foreach ( $totals as $type => $data ) {
|
||||
if ( 0 === $data['count'] && 0.0 === $data['total'] ) {
|
||||
wc_get_logger()->warning( "WC_Subscriptions_Tracker::get_order_count_and_total_by_meta_key() returned 0 count and total for {$type} orders" );
|
||||
}
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets count and total for initial orders (orders without subscription relation meta keys).
|
||||
*
|
||||
* @return array Array with 'count' and 'total' keys.
|
||||
*/
|
||||
private static function get_initial_order_count_and_total() {
|
||||
global $wpdb;
|
||||
|
||||
$order_statuses = array( 'wc-completed', 'wc-refunded' );
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
if ( wcs_is_custom_order_tables_usage_enabled() ) {
|
||||
// HPOS: Use wc_orders table
|
||||
$orders_table = $wpdb->prefix . 'wc_orders';
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
SUM( orders.total_amount ) AS 'total', COUNT( DISTINCT orders.id ) as 'count'
|
||||
FROM {$orders_table} AS orders
|
||||
RIGHT JOIN {$orders_table} AS subscriptions ON subscriptions.parent_order_id = orders.id
|
||||
WHERE orders.status in ( %s, %s )
|
||||
AND subscriptions.type = 'shop_subscription'
|
||||
AND orders.type = 'shop_order'",
|
||||
$order_statuses
|
||||
);
|
||||
} else {
|
||||
// CPT: Use posts and postmeta tables.
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT
|
||||
SUM( order_total.meta_value ) AS 'total', COUNT( * ) as 'count'
|
||||
FROM {$wpdb->posts} AS orders
|
||||
RIGHT JOIN {$wpdb->posts} AS subscriptions ON subscriptions.post_parent = orders.ID
|
||||
RIGHT JOIN {$wpdb->postmeta} AS order_total ON order_total.post_id = orders.ID
|
||||
WHERE orders.post_status in ( %s, %s )
|
||||
AND subscriptions.post_type = 'shop_subscription'
|
||||
AND orders.post_type = 'shop_order'
|
||||
AND order_total.meta_key = '_order_total'",
|
||||
$order_statuses
|
||||
);
|
||||
}
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
|
||||
|
||||
$result = $wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
|
||||
$totals = array(
|
||||
'count' => (int) ( $result ? $result['count'] : 0 ),
|
||||
'total' => (float) ( $result ? $result['total'] : 0 ),
|
||||
);
|
||||
|
||||
if ( 0 === $totals['count'] && 0.0 === $totals['total'] ) {
|
||||
wc_get_logger()->warning( 'WC_Subscriptions_Tracker::get_initial_order_count_and_total() returned 0 count and total' );
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets first and last subscription created dates.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -22,17 +22,15 @@ class WCS_Blocks_Integration implements IntegrationInterface {
|
|||
*/
|
||||
public function initialize() {
|
||||
$script_path = 'build/index.js';
|
||||
$style_path = 'build/index.css';
|
||||
|
||||
$script_url = \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( $script_path );
|
||||
$style_url = \WC_Subscriptions_Core_Plugin::instance()->get_subscriptions_core_directory_url( $style_path );
|
||||
|
||||
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/index.asset.php' );
|
||||
$script_asset = file_exists( $script_asset_path )
|
||||
? require $script_asset_path
|
||||
: array(
|
||||
'dependencies' => array(),
|
||||
'version' => $this->get_file_version( $script_asset_path ),
|
||||
'version' => self::get_file_version( $script_asset_path ),
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
|
|
@ -47,13 +45,6 @@ class WCS_Blocks_Integration implements IntegrationInterface {
|
|||
'woocommerce-subscriptions',
|
||||
\WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'languages' )
|
||||
);
|
||||
wp_enqueue_style(
|
||||
'wc-blocks-integration',
|
||||
$style_url,
|
||||
'',
|
||||
$this->get_file_version( \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/index.css' ) ),
|
||||
'all'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,7 +83,7 @@ class WCS_Blocks_Integration implements IntegrationInterface {
|
|||
* @param string $file Local path to the file.
|
||||
* @return string The cache buster value to use for the given file.
|
||||
*/
|
||||
protected function get_file_version( $file ) {
|
||||
public static function get_file_version( $file ) {
|
||||
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $file ) ) {
|
||||
return filemtime( $file );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,7 +237,12 @@ class WCS_Cached_Data_Manager extends WCS_Cache_Manager {
|
|||
protected function purge_subscription_user_cache( $subscription_id ) {
|
||||
wcs_deprecated_argument( __METHOD__, '2.3.0', sprintf( __( 'Customer subscription caching is now handled by %1$s and %2$s.', 'woocommerce-subscriptions' ), 'WCS_Customer_Store_Cached_CPT', 'WCS_Post_Meta_Cache_Manager' ) ); // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment
|
||||
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription_user_id = $subscription->get_user_id();
|
||||
$this->log( sprintf(
|
||||
'Clearing cache for user ID %1$s on %2$s hook.',
|
||||
|
|
|
|||
|
|
@ -604,6 +604,10 @@ class WCS_Cart_Renewal {
|
|||
|
||||
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return $update_customer_data;
|
||||
}
|
||||
|
||||
$billing_address = array();
|
||||
if ( $checkout_object->checkout_fields['billing'] ) {
|
||||
foreach ( array_keys( $checkout_object->checkout_fields['billing'] ) as $field ) {
|
||||
|
|
@ -627,19 +631,6 @@ class WCS_Cart_Renewal {
|
|||
return $update_customer_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
|
||||
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable( $is_purchasable, $product ) {
|
||||
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_renewal' );
|
||||
return WCS_Limiter::is_purchasable_renewal( $is_purchasable, $product );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag payment of manual renewal orders via an extra URL param.
|
||||
*
|
||||
|
|
@ -1195,7 +1186,12 @@ class WCS_Cart_Renewal {
|
|||
$cart_renewal_item = $this->cart_contains();
|
||||
|
||||
if ( false !== $cart_renewal_item ) {
|
||||
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
|
||||
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription_updated = false;
|
||||
|
||||
foreach ( [ 'billing', 'shipping' ] as $address_type ) {
|
||||
|
|
@ -1231,6 +1227,10 @@ class WCS_Cart_Renewal {
|
|||
if ( false !== $cart_renewal_item ) {
|
||||
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
|
||||
|
||||
if ( ! $subscription ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Billing address is a required field.
|
||||
foreach ( $request['billing_address'] as $key => $value ) {
|
||||
if ( is_callable( [ $customer, "set_billing_$key" ] ) ) {
|
||||
|
|
|
|||
|
|
@ -186,19 +186,6 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal {
|
|||
return $cart_item_session_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a product is being marked as not purchasable because it is limited and the customer has a subscription,
|
||||
* but the current request is to resubscribe to the subscription, then mark it as purchasable.
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable( $is_purchasable, $product ) {
|
||||
_deprecated_function( __METHOD__, '2.1', 'WCS_Limiter::is_purchasable_renewal' );
|
||||
return WCS_Limiter::is_purchasable_renewal( $is_purchasable, $product );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the cart to see if it contains a subscription resubscribe item.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ class WCS_Failed_Scheduled_Action_Manager {
|
|||
// Log any exceptions caught by the exception listener in action logs.
|
||||
if ( ! empty( $context['exceptions'] ) ) {
|
||||
foreach ( $context['exceptions'] as $exception_message ) {
|
||||
// @phpstan-ignore-next-line
|
||||
ActionScheduler_Logger::instance()->log( $action_id, $exception_message );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ class WCS_Limiter {
|
|||
if ( wcs_is_frontend_request() || wcs_is_checkout_blocks_api_request() ) {
|
||||
add_filter( 'woocommerce_subscription_is_purchasable', __CLASS__ . '::is_purchasable_switch', 12, 2 );
|
||||
add_filter( 'woocommerce_subscription_variation_is_purchasable', __CLASS__ . '::is_purchasable_switch', 12, 2 );
|
||||
add_filter( 'woocommerce_subscription_is_purchasable', __CLASS__ . '::is_purchasable_renewal', 12, 2 );
|
||||
add_filter( 'woocommerce_subscription_variation_is_purchasable', __CLASS__ . '::is_purchasable_renewal', 12, 2 );
|
||||
add_filter( 'woocommerce_valid_order_statuses_for_order_again', array( __CLASS__, 'filter_order_again_statuses_for_limited_subscriptions' ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +57,43 @@ class WCS_Limiter {
|
|||
do_action( 'woocommerce_subscriptions_product_options_advanced' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the session contains a renewal for a given product.
|
||||
* Used for the pay for order flow.
|
||||
*
|
||||
* @param WC_Product $product The product to check.
|
||||
* @return bool
|
||||
*/
|
||||
private static function session_contains_renewal( $product ) {
|
||||
if ( ! empty( WC()->session->cart ) ) {
|
||||
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
|
||||
if ( (int) $product->get_id() === (int) $cart_item['product_id'] && isset( $cart_item['subscription_renewal'] ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the session contains a resubscribe for a given product.
|
||||
* Used for the pay for order flow with limited subscriptions products.
|
||||
*
|
||||
* @param WC_Product $product The product to check.
|
||||
* @return bool
|
||||
*/
|
||||
private static function session_contains_resubscribe( $product ) {
|
||||
if ( ! empty( WC()->session->cart ) ) {
|
||||
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
|
||||
if ( (int) $product->get_id() === (int) $cart_item['product_id'] && isset( $cart_item['subscription_resubscribe'] ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical is_purchasable method to be called by product classes.
|
||||
*
|
||||
|
|
@ -69,37 +104,66 @@ class WCS_Limiter {
|
|||
* @return bool
|
||||
*/
|
||||
public static function is_purchasable( $purchasable, $product ) {
|
||||
switch ( $product->get_type() ) {
|
||||
case 'subscription':
|
||||
case 'variable-subscription':
|
||||
if ( true === $purchasable && false === self::is_purchasable_product( $purchasable, $product ) ) {
|
||||
$purchasable = false;
|
||||
|
||||
// Check if product is private (for variations, also check parent product)
|
||||
$is_private_product = 'private' === $product->get_status();
|
||||
if ( $product->get_parent_id() > 0 ) {
|
||||
$parent_product = wc_get_product( $product->get_parent_id() );
|
||||
if ( $parent_product ) {
|
||||
$is_private_product = 'private' === $parent_product->get_status();
|
||||
}
|
||||
}
|
||||
|
||||
// Checks limits for variable subscription products.
|
||||
if ( $product->get_type() === 'subscription_variation' && isset( $parent_product ) ) {
|
||||
|
||||
if ( $is_private_product ) {
|
||||
$purchasable = false;
|
||||
|
||||
// Forces product to be available when processing a renewal order. Allowing people to renew private products.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
|
||||
if ( self::is_paying_for_failed_renewal_order( $parent_product ) || isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() || self::session_contains_renewal( $parent_product ) ) {
|
||||
$purchasable = true;
|
||||
}
|
||||
break;
|
||||
case 'subscription_variation':
|
||||
$variable_product = wc_get_product( $product->get_parent_id() );
|
||||
}
|
||||
|
||||
if ( 'no' != wcs_get_product_limitation( $variable_product ) && ! empty( WC()->cart->cart_contents ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
|
||||
|
||||
// When mixed checkout is disabled, the variation is replaceable
|
||||
if ( 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) {
|
||||
$purchasable = true;
|
||||
} else { // When mixed checkout is enabled
|
||||
foreach ( WC()->cart->cart_contents as $cart_item ) {
|
||||
// If the variable product is limited, it can't be purchased if it is the same variation
|
||||
if ( $product->get_parent_id() === $cart_item['data']->get_parent_id() && $product->get_id() !== $cart_item['data']->get_id() ) {
|
||||
$purchasable = false;
|
||||
break;
|
||||
}
|
||||
if ( 'no' !== wcs_get_product_limitation( $parent_product ) && ( ! empty( WC()->cart->cart_contents ) || self::session_contains_resubscribe( $parent_product ) ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
|
||||
// When mixed checkout is disabled, the variation is replaceable.
|
||||
if ( 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) {
|
||||
foreach ( WC()->cart->cart_contents as $cart_item ) {
|
||||
// If the variable product is limited, it can't be purchased if it is the same variation
|
||||
if ( $product->get_parent_id() === $cart_item['data']->get_parent_id() && $product->get_id() !== $cart_item['data']->get_id() ) {
|
||||
$purchasable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else { // Checks limits for simple subscription products.
|
||||
if ( $is_private_product ) {
|
||||
$purchasable = false;
|
||||
|
||||
// Forces product to be available when processing a renewal order. Allowing people to renew private products.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
|
||||
if ( self::is_paying_for_failed_renewal_order( $product ) || isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() || self::session_contains_renewal( $product ) ) {
|
||||
$purchasable = true;
|
||||
}
|
||||
}
|
||||
|
||||
// This actually means the product is limited when returning false.
|
||||
if ( false === self::is_product_limited( $purchasable, $product ) ) {
|
||||
$resubscribe_cart_item = wcs_cart_contains_resubscribe();
|
||||
// Allows the product to be resubscribed but not purchased again.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
|
||||
if ( empty( $_GET['resubscribe'] ) && false === $resubscribe_cart_item && false === self::session_contains_resubscribe( $product ) ) {
|
||||
$purchasable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $purchasable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If a product is limited and the customer already has a subscription, mark it as not purchasable.
|
||||
*
|
||||
|
|
@ -107,6 +171,17 @@ class WCS_Limiter {
|
|||
* @return bool
|
||||
*/
|
||||
public static function is_purchasable_product( $is_purchasable, $product ) {
|
||||
_deprecated_function( __METHOD__, '8.0.0', 'WCS_Limiter::is_product_limited' );
|
||||
return self::is_product_limited( $is_purchasable, $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* If a product is limited and the customer already has a subscription, mark it as not purchasable.
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.1, Moved from WC_Subscriptions_Product
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_product_limited( $is_purchasable, $product ) {
|
||||
|
||||
// Set up cache
|
||||
if ( ! isset( self::$is_purchasable_cache[ $product->get_id() ] ) ) {
|
||||
|
|
@ -197,33 +272,9 @@ class WCS_Limiter {
|
|||
* @return bool
|
||||
*/
|
||||
public static function is_purchasable_renewal( $is_purchasable, $product ) {
|
||||
if ( false === $is_purchasable && false === self::is_purchasable_product( $is_purchasable, $product ) ) {
|
||||
$resubscribe_cart_item = wcs_cart_contains_resubscribe();
|
||||
_deprecated_function( __METHOD__, '8.0.0', 'WCS_Limiter::is_product_limited' );
|
||||
|
||||
// Resubscribe logic
|
||||
if ( isset( $_GET['resubscribe'] ) || false !== $resubscribe_cart_item ) {
|
||||
$subscription_id = ( isset( $_GET['resubscribe'] ) ) ? absint( $_GET['resubscribe'] ) : $resubscribe_cart_item['subscription_resubscribe']['subscription_id'];
|
||||
$subscription = wcs_get_subscription( $subscription_id );
|
||||
|
||||
if ( false != $subscription && $subscription->has_product( $product->get_id() ) && wcs_can_user_resubscribe_to( $subscription ) ) {
|
||||
$is_purchasable = true;
|
||||
}
|
||||
|
||||
// Renewal logic
|
||||
} elseif ( isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() ) {
|
||||
$is_purchasable = true;
|
||||
|
||||
// Restoring cart from session, so need to check the cart in the session (wcs_cart_contains_renewal() only checks the cart)
|
||||
} elseif ( ! empty( WC()->session->cart ) ) {
|
||||
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
|
||||
if ( $product->get_id() == $cart_item['product_id'] && ( isset( $cart_item['subscription_renewal'] ) || isset( $cart_item['subscription_resubscribe'] ) ) ) {
|
||||
$is_purchasable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $is_purchasable;
|
||||
return ! self::is_product_limited( $is_purchasable, $product );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -277,12 +328,73 @@ class WCS_Limiter {
|
|||
return self::$order_awaiting_payment_for_product[ $product_id ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're currently paying for a failed renewal order containing the product.
|
||||
*
|
||||
* @since 8.3.0 - Migrated from WooCommerce Subscriptions v2.1.0
|
||||
* @param WC_Product $product The product to check.
|
||||
* @return bool
|
||||
*/
|
||||
protected static function is_paying_for_failed_renewal_order( $product ) {
|
||||
global $wp;
|
||||
|
||||
// Check if we're on the pay for order page
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['pay_for_order'] ) || ! isset( $_GET['key'] ) || ! isset( $wp->query_vars['order-pay'] ) ) {
|
||||
// Also check if cart contains a failed renewal order payment
|
||||
$failed_renewal_cart_item = wcs_cart_contains_failed_renewal_order_payment();
|
||||
if ( false !== $failed_renewal_cart_item ) {
|
||||
$cart_item_product_id = isset( $failed_renewal_cart_item['variation_id'] ) && $failed_renewal_cart_item['variation_id'] > 0
|
||||
? $failed_renewal_cart_item['variation_id']
|
||||
: $failed_renewal_cart_item['product_id'];
|
||||
// Check both the product ID and parent product ID (for variations)
|
||||
if ( (int) $product->get_id() === (int) $cart_item_product_id || (int) $product->get_id() === (int) $failed_renewal_cart_item['product_id'] ) {
|
||||
return true;
|
||||
}
|
||||
// Also check if product is a variation and matches the parent
|
||||
if ( $product->get_parent_id() > 0 && (int) $product->get_parent_id() === (int) $failed_renewal_cart_item['product_id'] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : '';
|
||||
$order_id = isset( $wp->query_vars['order-pay'] ) ? $wp->query_vars['order-pay'] : 0;
|
||||
$order = wc_get_order( absint( $order_id ) );
|
||||
|
||||
if ( ! $order || $order->get_order_key() !== $order_key ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if order is a failed renewal order
|
||||
if ( ! $order->has_status( 'failed' ) && ! wcs_order_contains_renewal( $order ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the order contains the product
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$item_product_id = isset( $item['variation_id'] ) && $item['variation_id'] > 0 ? $item['variation_id'] : $item['product_id'];
|
||||
// Check both the product ID and parent product ID (for variations)
|
||||
if ( (int) $product->get_id() === (int) $item_product_id || (int) $product->get_id() === (int) $item['product_id'] ) {
|
||||
return true;
|
||||
}
|
||||
// Also check if product is a variation and matches the parent
|
||||
if ( $product->get_parent_id() > 0 && (int) $product->get_parent_id() === (int) $item['product_id'] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the order statuses that enable the order again button and functionality.
|
||||
*
|
||||
* This function will return no statuses if the order contains non purchasable or limited products.
|
||||
*
|
||||
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v3.0.2
|
||||
* @since 8.3.0 - Migrated from WooCommerce Subscriptions v3.0.2
|
||||
*
|
||||
* @param array $statuses The order statuses that enable the order again button.
|
||||
* @return array $statuses An empty array if the order contains limited products, otherwise the default statuses are returned.
|
||||
|
|
|
|||
|
|
@ -51,6 +51,13 @@ class WCS_Modal {
|
|||
*/
|
||||
private $actions = array();
|
||||
|
||||
/**
|
||||
* A unique ID for the modal to use in HTML ID attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id = '';
|
||||
|
||||
/**
|
||||
* Registers the scripts and stylesheets needed to display the modals.
|
||||
*
|
||||
|
|
@ -104,6 +111,7 @@ class WCS_Modal {
|
|||
$this->trigger = $trigger;
|
||||
$this->heading = $heading;
|
||||
$this->actions = $actions;
|
||||
$this->id = wp_unique_id( 'wcs-modal-' );
|
||||
|
||||
// Allow callers to provide the callback without any parameters. Assuming the content provided is the callback.
|
||||
if ( 'callback' === $this->content_type && ! isset( $content['parameters'] ) ) {
|
||||
|
|
@ -229,6 +237,30 @@ class WCS_Modal {
|
|||
return $this->trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the modal's unique ID.
|
||||
*
|
||||
* This is used for the actual HTML ID attribute, and so should follow the normal CSS identifier rules.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*
|
||||
* @param string $id The modal's unique ID.
|
||||
*/
|
||||
public function set_id( $id ) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modal's unique ID.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*
|
||||
* @return string The modal's unique ID.
|
||||
*/
|
||||
public function get_id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flattened string of HTML element attributes from an array of attributes and values.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -187,14 +187,20 @@ class WCS_My_Account_Payment_Methods {
|
|||
return;
|
||||
}
|
||||
|
||||
// translators: 1: token display name, 2: opening link tag, 4: closing link tag, 3: opening link tag.
|
||||
$notice = sprintf( esc_html__( 'Would you like to update your subscriptions to use this new payment method - %1$s?%2$sYes%4$s | %3$sNo%4$s', 'woocommerce-subscriptions' ),
|
||||
$notice = sprintf(
|
||||
// translators: 1: token display name, 2: opening link tag, 4: closing link tag, 3: opening link tag.
|
||||
esc_html__( 'Would you like to update your subscriptions to use this new payment method - %1$s?%2$sYes%4$s | %3$sNo%4$s', 'woocommerce-subscriptions' ),
|
||||
$default_token->get_display_name(),
|
||||
'</br><a href="' . esc_url( add_query_arg( array(
|
||||
'update-subscription-tokens' => 'true',
|
||||
'token-id' => $default_token_id,
|
||||
'_wcsnonce' => wp_create_nonce( 'wcs-update-subscription-tokens' ),
|
||||
), wc_get_account_endpoint_url( 'payment-methods' ) ) ) . '"><strong>',
|
||||
'<br><a href="' . esc_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'update-subscription-tokens' => 'true',
|
||||
'token-id' => $default_token_id,
|
||||
'_wcsnonce' => wp_create_nonce( 'wcs-update-subscription-tokens' ),
|
||||
),
|
||||
wc_get_account_endpoint_url( 'payment-methods' )
|
||||
)
|
||||
) . '"><strong>',
|
||||
'<a href=""><strong>',
|
||||
'</strong></a>'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -92,11 +92,9 @@ class WCS_Object_Data_Cache_Manager extends WCS_Post_Meta_Cache_Manager {
|
|||
}
|
||||
|
||||
$force_all_fields = 'all_fields' === $generate_type;
|
||||
// @phpstan-ignore-next-line
|
||||
$changes = $subscription->get_changes();
|
||||
$base_data = $subscription->get_base_data();
|
||||
// @phpstan-ignore-next-line
|
||||
$meta_data = $subscription->get_meta_data();
|
||||
$changes = $subscription->get_changes();
|
||||
$base_data = $subscription->get_base_data();
|
||||
$meta_data = $subscription->get_meta_data();
|
||||
|
||||
// Deleted meta won't be included in the changes, so we need to fetch the previous value via the raw meta data.
|
||||
$data_store = $subscription->get_data_store();
|
||||
|
|
@ -144,7 +142,6 @@ class WCS_Object_Data_Cache_Manager extends WCS_Post_Meta_Cache_Manager {
|
|||
} elseif ( $meta->get_changes() ) {
|
||||
// If the value is being updated.
|
||||
$this->object_changes[ $subscription->get_id() ][ $data_key ] = [
|
||||
// @phpstan-ignore-next-line
|
||||
'new' => $meta->value,
|
||||
'previous' => isset( $previous_meta['value'] ) ? $previous_meta['value'] : null,
|
||||
'type' => 'update',
|
||||
|
|
@ -152,7 +149,6 @@ class WCS_Object_Data_Cache_Manager extends WCS_Post_Meta_Cache_Manager {
|
|||
} elseif ( $force_all_fields ) {
|
||||
// If we're forcing all fields to be recorded.
|
||||
$this->object_changes[ $subscription->get_id() ][ $data_key ] = [
|
||||
// @phpstan-ignore-next-line
|
||||
'new' => $meta->value,
|
||||
'type' => 'add',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ class WCS_Query extends WC_Query {
|
|||
|
||||
add_filter( 'the_title', array( $this, 'change_endpoint_title' ), 11, 1 );
|
||||
|
||||
add_filter( 'woocommerce_get_query_vars', array( $this, 'add_wcs_query_vars' ) );
|
||||
|
||||
if ( ! is_admin() ) {
|
||||
add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
|
||||
add_action( 'parse_request', array( $this, 'parse_request' ), 0 );
|
||||
add_action( 'pre_get_posts', array( $this, 'maybe_redirect_payment_methods' ) );
|
||||
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 11 );
|
||||
add_filter( 'woocommerce_get_query_vars', array( $this, 'add_wcs_query_vars' ) );
|
||||
|
||||
// Inserting your new tab/page into the My Account page.
|
||||
add_filter( 'woocommerce_account_menu_items', array( $this, 'add_menu_items' ) );
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ class WCS_Staging {
|
|||
if ( 'subscriptions_install' === $source ) {
|
||||
$site_url = self::get_live_site_url();
|
||||
} elseif ( ! is_multisite() && defined( 'WP_SITEURL' ) ) {
|
||||
$site_url = WP_SITEURL;
|
||||
$site_url = WP_SITEURL; // @phpstan-ignore phpstanWP.wpConstant.fetch (Using constant for performance and staging-specific behavior)
|
||||
} else {
|
||||
$site_url = get_site_url();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ class WC_Subscriptions_Email_Preview {
|
|||
case 'WCS_Email_Customer_Notification_Auto_Renewal':
|
||||
$email->set_object( $this->get_dummy_subscription() );
|
||||
break;
|
||||
case 'WCSG_Email_Recipient_New_Initial_Order':
|
||||
$email->set_object( $this->get_dummy_subscription() );
|
||||
$email->subscriptions = [ $this->get_dummy_subscription() ];
|
||||
break;
|
||||
case 'WCS_Email_Customer_Payment_Retry':
|
||||
case 'WCS_Email_Payment_Retry':
|
||||
$email->retry = $this->get_dummy_retry( $email->object );
|
||||
|
|
@ -191,8 +195,7 @@ class WC_Subscriptions_Email_Preview {
|
|||
* @return bool Whether the email being previewed is a subscription email.
|
||||
*/
|
||||
private function is_subscription_email() {
|
||||
return isset( WC_Subscriptions_Email::$email_classes[ $this->email_type ] )
|
||||
|| isset( WC_Subscriptions_Email_Notifications::$email_classes[ $this->email_type ] )
|
||||
return isset( apply_filters( 'wcs_email_classes', array_merge( WC_Subscriptions_Email::$email_classes, WC_Subscriptions_Email_Notifications::$email_classes ) )[ $this->email_type ] )
|
||||
|| in_array( $this->email_type, [ 'WCS_Email_Customer_Payment_Retry', 'WCS_Email_Payment_Retry' ], true );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.1
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
* @author Prospress
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WCS_Email_Cancelled_Subscription extends WC_Email {
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.0.0
|
||||
* @package WooCommerce/Classes/Emails
|
||||
* @author Prospress
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Order {
|
||||
|
||||
|
|
@ -163,26 +162,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.0.0
|
||||
* @package WooCommerce/Classes/Emails
|
||||
* @author Prospress
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order {
|
||||
|
||||
|
|
@ -172,25 +171,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* @class WCS_Email_Customer_Notification_Auto_Renewal
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Auto_Renewal extends WCS_Email_Customer_Notification {
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* @class WCS_Email_Customer_Notification_Free_Trial_Expiry
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Auto_Trial_Expiration extends WCS_Email_Customer_Notification {
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* @class WCS_Email_Customer_Notification_Manual_Renewal
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Manual_Renewal extends WCS_Email_Customer_Notification {
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* @class WCS_Email_Customer_Notification_Free_Trial_Expiry
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Manual_Trial_Expiration extends WCS_Email_Customer_Notification {
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
* @class WCS_Email_Customer_Notification_Subscription_Expiring
|
||||
* @version 1.0.0
|
||||
* @package WooCommerce_Subscriptions/Classes/Emails
|
||||
* @extends WC_Email
|
||||
*/
|
||||
class WCS_Email_Customer_Notification_Subscription_Expiration extends WCS_Email_Customer_Notification {
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue