| @@ -57,9 +57,9 @@ | ||
| 57 | 57 | } |
| 58 | 58 | |
| 59 | 59 | if(!empty($client_id) && !empty($secret)) |
| 60 | 60 | { |
| 61 | - $scheme = strtolower((string)wp_parse_url(home_url('/'), PHP_URL_SCHEME)); | |
| 61 | + $scheme = strtolower((string)c_ws_plugin__s2member_utils_urls::parse_url(home_url('/'), PHP_URL_SCHEME)); | |
| 62 | 62 | |
| 63 | 63 | //260224 Reverse-proxy/Cloudflare fallback for admin-side HTTPS detection. |
| 64 | 64 | // This only affects the admin gating check here. It does NOT change the webhook URL itself. |
| 65 | 65 | $proxy_https = false; |
| @@ -84,9 +84,14 @@ | ||
| 84 | 84 | |
| 85 | 85 | $r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_webhook_upsert($env); |
| 86 | 86 | |
| 87 | 87 | if(!empty($r['id'])) |
| 88 | + { | |
| 88 | 89 | $ppco_webhook_notice = '<div class="updated"><p>'.sprintf(esc_html__('PayPal Checkout %1$s webhook created/updated successfully.', 's2member'), $env_label).'</p></div>'."\n"; |
| 90 | + | |
| 91 | + //260824.0507 The persistent warning was rendered before this page action ran; remove that now-resolved copy immediately. | |
| 92 | + $ppco_webhook_notice .= '<script>(function(){var e=document.querySelector(".s2member-ppco-webhook-upgrade-notice-'.esc_js($env).'");if(e&&e.closest){e=e.closest(".notice");if(e)e.remove();}})();</script>'."\n"; | |
| 93 | + } | |
| 89 | 94 | else |
| 90 | 95 | $ppco_webhook_notice = '<div class="error"><p>'.sprintf(esc_html__('Unable to create/update PayPal Checkout %1$s webhook. See the s2Member paypal-checkout.log for details.', 's2member'), $env_label).' <a href="'.esc_attr(admin_url("/admin.php?page=ws-plugin--s2member-logs")).'">'.esc_html__('Log Viewer', 's2member').'</a></p></div>'."\n"; |
| 91 | 96 | } |
| 92 | 97 | } |
| @@ -184,16 +189,19 @@ | ||
| 184 | 189 | if(apply_filters("ws_plugin__s2member_during_paypal_ops_page_during_left_sections_display_paypal_account_details", TRUE, get_defined_vars())) |
| 185 | 190 | { |
| 186 | 191 | do_action("ws_plugin__s2member_during_paypal_ops_page_during_left_sections_before_paypal_account_details", get_defined_vars()); |
| 187 | 192 | |
| 188 | - //260106 | |
| 189 | - echo '<div class="ws-menu-page-group" title="PayPal Checkout (Beta)">'."\n"; | |
| 193 | + //260106 New PayPal Checkout panel. | |
| 194 | + //260824.0507 Allow administrative notices to open and jump directly to the PayPal Checkout panel. | |
| 195 | + echo '<div id="ws-plugin--s2member-paypal-checkout"></div>'."\n"; | |
| 196 | + echo '<div class="ws-menu-page-group" title="PayPal Checkout (Beta)"'.((!empty($_GET['s2member-open-panel']) && $_GET['s2member-open-panel'] === 'paypal-checkout') ? ' default-state="open"' : '').'>'."\n"; | |
| 190 | 197 | |
| 191 | 198 | echo '<div class="ws-menu-page-section ws-plugin--s2member-paypal-checkout-account-details-section">'."\n"; |
| 192 | 199 | echo '<a href="https://s2member.com/r/paypal/" target="_blank"><img src="'.esc_attr($GLOBALS["WS_PLUGIN__"]["s2member"]["c"]["dir_url"]).'/src/images/paypal-pp-logo-200px.png" class="ws-menu-page-right s2m-ppco-paypal-logo" style="width:250px; max-width:25%; height:auto; margin:0 20px 20px 0;" alt="PayPal" /></a>'."\n"; |
| 193 | 200 | |
| 194 | 201 | echo '<div class="ws-menu-page-notice ws-menu-page-notice-info">'."\n"; |
| 195 | - echo '<p>'.esc_html__('When enabled, existing s2Member PayPal Button shortcodes are powered by PayPal Checkout without requiring shortcode edits. If disabled, s2Member continues using PayPal Standard.', 's2member').'</p>'."\n"; | |
| 202 | + //260818.2340 PayPal Checkout now powers both Framework buttons and the PayPal option in s2Member Pro-Forms without shortcode edits. | |
| 203 | + echo '<p>'.esc_html__('When enabled, existing s2Member PayPal Button shortcodes and s2Member Pro-Forms that offer PayPal are powered by PayPal Checkout without requiring shortcode edits. If disabled, they continue using their existing PayPal Standard or Express Checkout flow.', 's2member').'</p>'."\n"; | |
| 196 | 204 | |
| 197 | 205 | echo '<p style="margin:0 0 6px 0;"><strong>'.esc_html__('Quick Setup (Live or Sandbox)', 's2member').'</strong></p>'."\n"; |
| 198 | 206 | echo '<ol style="margin:0 0 0 20px;">'."\n"; |
| 199 | 207 | echo '<li>'.esc_html__('Enable PayPal Checkout below, choose the environment (Live or Sandbox).', 's2member').'</li>'."\n"; |
| @@ -213,14 +221,14 @@ | ||
| 213 | 221 | echo '</ul>'."\n"; |
| 214 | 222 | |
| 215 | 223 | echo '</div>'."\n"; |
| 216 | 224 | |
| 217 | - echo '<p><em>'.esc_html__('Leave these fields blank to continue using PayPal Standard.', 's2member').'</em></p>'."\n"; | |
| 225 | + echo '<p><em>'.esc_html__('Leave these fields blank to continue using your existing PayPal Standard and Express Checkout integrations.', 's2member').'</em></p>'."\n"; | |
| 218 | 226 | |
| 219 | 227 | $ppco_https_ready = true; |
| 220 | 228 | $ppco_https_reason = ''; |
| 221 | 229 | |
| 222 | - $ppco_https_scheme = strtolower((string)wp_parse_url(home_url('/'), PHP_URL_SCHEME)); | |
| 230 | + $ppco_https_scheme = strtolower((string)c_ws_plugin__s2member_utils_urls::parse_url(home_url('/'), PHP_URL_SCHEME)); | |
| 223 | 231 | |
| 224 | 232 | //260224 Reverse-proxy/Cloudflare fallback for admin-side HTTPS detection. |
| 225 | 233 | // This keeps PayPal Checkout configuration available when WordPress is behind a proxy |
| 226 | 234 | // and the current admin request is clearly HTTPS at the edge. |
| @@ -270,9 +278,9 @@ | ||
| 270 | 278 | echo '<tr>'."\n"; |
| 271 | 279 | |
| 272 | 280 | echo '<td>'."\n"; |
| 273 | 281 | echo '<input type="radio" name="ws_plugin__s2member_paypal_checkout_enable" id="ws-plugin--s2member-paypal-checkout-enable-0" value="0" onclick="window.s2mPpcoApplyEnvCues&&window.s2mPpcoApplyEnvCues();"'.((!$GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["paypal_checkout_enable"]) ? ' checked="checked"' : '').' /> <label for="ws-plugin--s2member-paypal-checkout-enable-0">No</label> <input type="radio" name="ws_plugin__s2member_paypal_checkout_enable" id="ws-plugin--s2member-paypal-checkout-enable-1" value="1" onclick="window.s2mPpcoApplyEnvCues&&window.s2mPpcoApplyEnvCues();"'.((!$ppco_https_ready) ? ' disabled="disabled"' : '').(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["paypal_checkout_enable"]) ? ' checked="checked"' : '').' /> <label for="ws-plugin--s2member-paypal-checkout-enable-1">Yes, enable PayPal Checkout.</label><br />'."\n"; |
| 274 | - echo '<em>Only enable this after you have configured PayPal Checkout credentials below. If disabled, s2Member continues using PayPal Standard.</em>'."\n"; | |
| 282 | + echo '<em>Only enable this after you have configured PayPal Checkout credentials below. If disabled, s2Member continues using the existing PayPal Standard and Express Checkout integrations.</em>'."\n"; | |
| 275 | 283 | |
| 276 | 284 | if(!$ppco_https_ready) |
| 277 | 285 | echo '<br /><span class="ws-menu-page-error">'.esc_html__('Disabled: PayPal Checkout requires HTTPS (SSL) for webhooks. Fix HTTPS for this domain, then enable PayPal Checkout.', 's2member').'</span>'."\n"; |
| 278 | 286 | echo '</td>'."\n"; |
| @@ -491,9 +499,9 @@ | ||
| 491 | 499 | else |
| 492 | 500 | echo '<span class="button button-primary" style="min-width:250px; text-align:center; color:#fff !important; opacity:0.50; cursor:default; pointer-events:none;" title="'.esc_attr__('Save Live credentials first.', 's2member').'">'.esc_html__('Create/Update Webhook', 's2member').'</span>'."\n"; |
| 493 | 501 | echo '</th>'."\n"; |
| 494 | 502 | echo '<td style="padding:0;">'."\n"; |
| 495 | - echo '<em>'.esc_html__('Registers your webhook URL with PayPal (HTTPS required).', 's2member').' '.esc_html__('If your site URL changes, re-run Create/Update Webhook to update the callback URL at PayPal.', 's2member').'</em>'."\n"; | |
| 503 | + echo '<em>'.esc_html__('Registers or updates your webhook URL and required events with PayPal (HTTPS required).', 's2member').' '.esc_html__('If your site URL changes, re-run Create/Update Webhook to update the callback URL at PayPal.', 's2member').'</em>'."\n"; | |
| 496 | 504 | echo '</td>'."\n"; |
| 497 | 505 | echo '</tr>'."\n"; |
| 498 | 506 | |
| 499 | 507 | echo '<tr>'."\n"; |
| @@ -519,9 +527,9 @@ | ||
| 519 | 527 | echo '<br /><br />'."\n"; |
| 520 | 528 | echo '<em>'.esc_html__('Webhook URL:', 's2member').'</em><br /><code>'.esc_html(add_query_arg("s2member_paypal_webhook", "1", home_url("/", 'https'))).'</code>'."\n"; |
| 521 | 529 | echo '<br /><em class="ws-menu-page-hilite">'.esc_html__('Note: PayPal requires an HTTPS (SSL) webhook URL.', 's2member').'</em>'."\n"; |
| 522 | 530 | |
| 523 | - $ppco_https_scheme = strtolower((string)wp_parse_url(home_url('/'), PHP_URL_SCHEME)); | |
| 531 | + $ppco_https_scheme = strtolower((string)c_ws_plugin__s2member_utils_urls::parse_url(home_url('/'), PHP_URL_SCHEME)); | |
| 524 | 532 | |
| 525 | 533 | //260224 Reverse-proxy/Cloudflare fallback for admin-side HTTPS warning display. |
| 526 | 534 | $ppco_proxy_https = false; |
| 527 | 535 | if(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && stripos(strtolower((string)$_SERVER['HTTP_X_FORWARDED_PROTO']), 'https') !== false) |
| @@ -548,10 +556,10 @@ | ||
| 548 | 556 | else if($ppco_https_code && $ppco_https_code >= 400) |
| 549 | 557 | echo '<br /><span class="ws-menu-page-error">'.sprintf(esc_html__('Warning: Server-side probe to your HTTPS endpoint returned HTTP %1$s. This can be caused by SSL/virtual-host configuration issues, but it can also be a false negative due to security rules that block loopback requests. Use the “Test Webhook Endpoint” link and PayPal\'s webhook simulator to confirm external reachability.', 's2member'), (int)$ppco_https_code).'</span>'."\n"; |
| 550 | 558 | } |
| 551 | 559 | |
| 552 | - //260216 Include additional events required for refunds/reversals and subscription lifecycle. | |
| 553 | - echo '<br /><br /><em>'.esc_html__('Required Events:', 's2member').'</em><br /><code>PAYMENT.SALE.COMPLETED</code> <code>PAYMENT.CAPTURE.COMPLETED</code> <code>PAYMENT.SALE.REFUNDED</code> <code>PAYMENT.CAPTURE.REFUNDED</code> <code>PAYMENT.SALE.REVERSED</code> <code>PAYMENT.CAPTURE.REVERSED</code> <code>BILLING.SUBSCRIPTION.CREATED</code> <code>BILLING.SUBSCRIPTION.ACTIVATED</code> <code>BILLING.SUBSCRIPTION.RE-ACTIVATED</code> <code>BILLING.SUBSCRIPTION.UPDATED</code> <code>BILLING.SUBSCRIPTION.CANCELLED</code> <code>BILLING.SUBSCRIPTION.SUSPENDED</code> <code>BILLING.SUBSCRIPTION.EXPIRED</code> <code>BILLING.SUBSCRIPTION.PAYMENT.FAILED</code>'."\n"; | |
| 560 | + //260824.1727 Include payment, dispute, and subscription lifecycle events required by Checkout processing. | |
| 561 | + echo '<br /><br /><em>'.esc_html__('Required Events:', 's2member').'</em><br /><code>PAYMENT.SALE.COMPLETED</code> <code>PAYMENT.CAPTURE.COMPLETED</code> <code>PAYMENT.SALE.REFUNDED</code> <code>PAYMENT.CAPTURE.REFUNDED</code> <code>PAYMENT.SALE.REVERSED</code> <code>PAYMENT.CAPTURE.REVERSED</code> <code>CUSTOMER.DISPUTE.CREATED</code> <code>BILLING.SUBSCRIPTION.CREATED</code> <code>BILLING.SUBSCRIPTION.ACTIVATED</code> <code>BILLING.SUBSCRIPTION.RE-ACTIVATED</code> <code>BILLING.SUBSCRIPTION.UPDATED</code> <code>BILLING.SUBSCRIPTION.CANCELLED</code> <code>BILLING.SUBSCRIPTION.SUSPENDED</code> <code>BILLING.SUBSCRIPTION.EXPIRED</code> <code>BILLING.SUBSCRIPTION.PAYMENT.FAILED</code>'."\n"; | |
| 554 | 562 | |
| 555 | 563 | echo '<p style="margin:10px 0 0 0;"><em>'.esc_html__('Tip: Use PayPal\'s Webhooks Events dashboard to view and re-deliver webhook events while testing.', 's2member').' <a href="https://developer.paypal.com/api/rest/webhooks/events-dashboard/" target="_blank" rel="external">'.esc_html__('Open Events Dashboard', 's2member').'</a></em></p>'."\n"; |
| 556 | 564 | |
| 557 | 565 | if(!$ppco_https_ready) |
| @@ -661,9 +669,9 @@ | ||
| 661 | 669 | else |
| 662 | 670 | echo '<span class="button button-primary" style="min-width:250px; text-align:center; color:#fff !important; opacity:0.50; cursor:default; pointer-events:none;" title="'.esc_attr__('Save Sandbox credentials first.', 's2member').'">'.esc_html__('Create/Update Webhook', 's2member').'</span>'."\n"; |
| 663 | 671 | echo '</th>'."\n"; |
| 664 | 672 | echo '<td style="padding:0;">'."\n"; |
| 665 | - echo '<em>'.esc_html__('Registers your webhook URL with PayPal (HTTPS required).', 's2member').' '.esc_html__('If your site URL changes, re-run Create/Update Webhook to update the callback URL at PayPal.', 's2member').'</em>'."\n"; | |
| 673 | + echo '<em>'.esc_html__('Registers or updates your webhook URL and required events with PayPal (HTTPS required).', 's2member').' '.esc_html__('If your site URL changes, re-run Create/Update Webhook to update the callback URL at PayPal.', 's2member').'</em>'."\n"; | |
| 666 | 674 | echo '</td>'."\n"; |
| 667 | 675 | echo '</tr>'."\n"; |
| 668 | 676 | |
| 669 | 677 | echo '<tr>'."\n"; |
| @@ -689,10 +697,10 @@ | ||
| 689 | 697 | echo '<br /><br />'."\n"; |
| 690 | 698 | echo '<em>'.esc_html__('Webhook URL:', 's2member').'</em><br /><code>'.esc_html(add_query_arg("s2member_paypal_webhook", "1", home_url("/", 'https'))).'</code>'."\n"; |
| 691 | 699 | echo '<br /><em class="ws-menu-page-hilite">'.esc_html__('Note: PayPal requires an HTTPS (SSL) webhook URL.', 's2member').'</em>'."\n"; |
| 692 | 700 | |
| 693 | - //260216 Include additional events required for refunds/reversals and subscription lifecycle. | |
| 694 | - echo '<br /><br /><em>'.esc_html__('Required Events:', 's2member').'</em><br /><code>PAYMENT.SALE.COMPLETED</code> <code>PAYMENT.CAPTURE.COMPLETED</code> <code>PAYMENT.SALE.REFUNDED</code> <code>PAYMENT.CAPTURE.REFUNDED</code> <code>PAYMENT.SALE.REVERSED</code> <code>PAYMENT.CAPTURE.REVERSED</code> <code>BILLING.SUBSCRIPTION.CREATED</code> <code>BILLING.SUBSCRIPTION.ACTIVATED</code> <code>BILLING.SUBSCRIPTION.RE-ACTIVATED</code> <code>BILLING.SUBSCRIPTION.UPDATED</code> <code>BILLING.SUBSCRIPTION.CANCELLED</code> <code>BILLING.SUBSCRIPTION.SUSPENDED</code> <code>BILLING.SUBSCRIPTION.EXPIRED</code> <code>BILLING.SUBSCRIPTION.PAYMENT.FAILED</code>'."\n"; | |
| 701 | + //260824.1727 Include payment, dispute, and subscription lifecycle events required by Checkout processing. | |
| 702 | + echo '<br /><br /><em>'.esc_html__('Required Events:', 's2member').'</em><br /><code>PAYMENT.SALE.COMPLETED</code> <code>PAYMENT.CAPTURE.COMPLETED</code> <code>PAYMENT.SALE.REFUNDED</code> <code>PAYMENT.CAPTURE.REFUNDED</code> <code>PAYMENT.SALE.REVERSED</code> <code>PAYMENT.CAPTURE.REVERSED</code> <code>CUSTOMER.DISPUTE.CREATED</code> <code>BILLING.SUBSCRIPTION.CREATED</code> <code>BILLING.SUBSCRIPTION.ACTIVATED</code> <code>BILLING.SUBSCRIPTION.RE-ACTIVATED</code> <code>BILLING.SUBSCRIPTION.UPDATED</code> <code>BILLING.SUBSCRIPTION.CANCELLED</code> <code>BILLING.SUBSCRIPTION.SUSPENDED</code> <code>BILLING.SUBSCRIPTION.EXPIRED</code> <code>BILLING.SUBSCRIPTION.PAYMENT.FAILED</code>'."\n"; | |
| 695 | 703 | |
| 696 | 704 | echo '<p style="margin:10px 0 0 0;"><em>'.esc_html__('Tip: Use PayPal\'s Webhooks Events dashboard to view and re-deliver webhook events while testing.', 's2member').' <a href="https://developer.paypal.com/api/rest/webhooks/events-dashboard/" target="_blank" rel="external">'.esc_html__('Open Events Dashboard', 's2member').'</a></em></p>'."\n"; |
| 697 | 705 | |
| 698 | 706 | if(!$ppco_https_ready) |
| @@ -1090,9 +1098,9 @@ | ||
| 1090 | 1098 | echo '<div id="ws-plugin--s2member-paypal-ipn-details" style="margin-top:10px; display:none;">'."\n"; |
| 1091 | 1099 | echo '<p><em><strong>IPN History:</strong> Here\'s PayPal\'s <a href="https://s2member.com/r/paypal-ipn-history/" target="_blank" rel="external">Instant Payment Notification (IPN) History page</a>.</em></p>'; |
| 1092 | 1100 | echo '<p><em><strong>Quick Tip:</strong> In addition to the <a href="http://s2member.com/r/paypal-com-ipn-configuration-page/" target="_blank" rel="external">default IPN settings inside your PayPal account</a>, the IPN URL is also set on a per-transaction basis by the special PayPal Button Code that s2Member provides you with. In other words, if you have multiple sites operating on one PayPal account, that\'s OK. s2Member dynamically sets the IPN URL for each transaction. The result is that the IPN URL configured from within your PayPal account, becomes the default, which is then overwritten on a per-transaction basis. In fact, PayPal recently updated their system to support IPN URL preservation. One PayPal account can handle multiple sites, all using different IPN URLs.</em></p>'."\n"; |
| 1093 | 1101 | do_action("ws_plugin__s2member_during_paypal_ops_page_during_left_sections_during_paypal_ipn_after_quick_tip", get_defined_vars()); |
| 1094 | - echo '<p><em><strong>IPN Communications:</strong> You\'ll be happy to know that s2Member handles cancellations, expirations, failed payments, terminations (e.g., refunds & chargebacks) for you automatically. If you log into your PayPal account and cancel a Member\'s Subscription, or, if the Member logs into their PayPal account and cancels their own Subscription, s2Member will be notified of these important changes and react accordingly through the PayPal IPN service that runs silently behind-the-scene. The PayPal IPN service will notify s2Member whenever a Member\'s payments have been failing, and/or whenever a Member\'s Subscription has expired for any reason. Even refunds & chargeback reversals are supported through the IPN service. If you issue a refund to an unhappy Customer through PayPal, s2Member will be notified, and the account for that Customer will either be demoted to a Free Subscriber, or deleted automatically (based on your configuration). The communication from PayPal → s2Member is seamless.</em></p>'."\n"; | |
| 1102 | + echo '<p><em><strong>IPN Communications:</strong> You\'ll be happy to know that s2Member handles cancellations, expirations, failed payments, terminations (e.g., refunds & chargebacks) for you automatically. If you log into your PayPal account and cancel a Member\'s Subscription, or, if the Member logs into their PayPal account and cancels their own Subscription, s2Member will be notified of these important changes and react accordingly through the PayPal IPN service that runs silently behind-the-scene. The PayPal IPN service will notify s2Member whenever a Member\'s payments have been failing, and/or whenever a Member\'s Subscription has expired for any reason. Even refunds & chargeback reversals are supported through the IPN service. If you issue a refund to an unhappy Customer through PayPal, s2Member will be notified, and the account for that Customer will either be assigned the role selected under Demote To Role, or have the configured Delete behavior applied automatically. The communication from PayPal → s2Member is seamless.</em></p>'."\n"; | |
| 1095 | 1103 | echo '</div>'."\n"; |
| 1096 | 1104 | |
| 1097 | 1105 | echo '<div class="ws-menu-page-hr"></div>'."\n"; |
| 1098 | 1106 | |
| @@ -1264,9 +1272,9 @@ | ||
| 1264 | 1272 | echo '<code>custom="'.esc_html($_SERVER["HTTP_HOST"]).'|christmas-promo"</code>'."\n"; |
| 1265 | 1273 | |
| 1266 | 1274 | echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ? |
| 1267 | 1275 | '<div class="ws-menu-page-hr"></div>'."\n". |
| 1268 | - '<p style="margin:0;"><strong>PHP Code:</strong> It is also possible to use PHP tags—optional (for developers). If you use PHP tags, please run a test email with <code><?php print_r(get_defined_vars()); ?></code>. This will give you a full list of all PHP variables available to you in this email. The <code>$paypal</code> variable is the most important one. It contains all of the <code>$_POST</code> variables received from PayPal\'s IPN service—or from an s2Member Pro-Form integration (e.g., <code>$paypal["item_number"]</code>, <code>$paypal["item_name"]</code>, etc). Please note that all Replacement Codes will be parsed first, and then any PHP tags that you\'ve included. Also, please remember that emails are sent in plain text format.</p>'."\n" | |
| 1276 | + '<p style="margin:0;"><strong>Advanced Customization:</strong> Further customization is possible with the <code>ws_plugin__s2member_signup_email_sbj</code> and <code>ws_plugin__s2member_signup_email_msg</code> filters from a plugin or theme.</p>'."\n" | |
| 1269 | 1277 | : ''; |
| 1270 | 1278 | echo '</td>'."\n"; |
| 1271 | 1279 | |
| 1272 | 1280 | echo '</tr>'."\n"; |
| @@ -1403,9 +1411,9 @@ | ||
| 1403 | 1411 | echo '<code>custom="'.esc_html($_SERVER["HTTP_HOST"]).'|christmas-promo"</code>'."\n"; |
| 1404 | 1412 | |
| 1405 | 1413 | echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ? |
| 1406 | 1414 | '<div class="ws-menu-page-hr"></div>'."\n". |
| 1407 | - '<p style="margin:0;"><strong>PHP Code:</strong> It is also possible to use PHP tags—optional (for developers). If you use PHP tags, please run a test email with <code><?php print_r(get_defined_vars()); ?></code>. This will give you a full list of all PHP variables available to you in this email. The <code>$paypal</code> variable is the most important one. It contains all of the <code>$_POST</code> variables received from PayPal\'s IPN service—or from an s2Member Pro-Form integration (e.g., <code>$paypal["item_number"]</code>, <code>$paypal["item_name"]</code>, etc). Please note that all Replacement Codes will be parsed first, and then any PHP tags that you\'ve included. Also, please remember that emails are sent in plain text format.</p>'."\n" | |
| 1415 | + '<p style="margin:0;"><strong>Advanced Customization:</strong> Further customization is possible with the <code>ws_plugin__s2member_modification_email_sbj</code> and <code>ws_plugin__s2member_modification_email_msg</code> filters from a plugin or theme.</p>'."\n" | |
| 1408 | 1416 | : ''; |
| 1409 | 1417 | echo '</td>'."\n"; |
| 1410 | 1418 | |
| 1411 | 1419 | echo '</tr>'."\n"; |
| @@ -1534,9 +1542,9 @@ | ||
| 1534 | 1542 | echo '<code>custom="'.esc_html($_SERVER["HTTP_HOST"]).'|christmas-promo"</code>'."\n"; |
| 1535 | 1543 | |
| 1536 | 1544 | echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ? |
| 1537 | 1545 | '<div class="ws-menu-page-hr"></div>'."\n". |
| 1538 | - '<p style="margin:0;"><strong>PHP Code:</strong> It is also possible to use PHP tags—optional (for developers). If you use PHP tags, please run a test email with <code><?php print_r(get_defined_vars()); ?></code>. This will give you a full list of all PHP variables available to you in this email. The <code>$paypal</code> variable is the most important one. It contains all of the <code>$_POST</code> variables received from PayPal\'s IPN service—or from an s2Member Pro-Form integration (e.g., <code>$paypal["item_number"]</code>, <code>$paypal["item_name"]</code>, etc). Please note that all Replacement Codes will be parsed first, and then any PHP tags that you\'ve included. Also, please remember that emails are sent in plain text format.</p>'."\n" | |
| 1546 | + '<p style="margin:0;"><strong>Advanced Customization:</strong> Further customization is possible with the <code>ws_plugin__s2member_capabilities_email_sbj</code> and <code>ws_plugin__s2member_capabilities_email_msg</code> filters from a plugin or theme.</p>'."\n" | |
| 1539 | 1547 | : ''; |
| 1540 | 1548 | echo '</td>'."\n"; |
| 1541 | 1549 | |
| 1542 | 1550 | echo '</tr>'."\n"; |
| @@ -1644,9 +1652,9 @@ | ||
| 1644 | 1652 | echo '<code>custom="'.esc_html($_SERVER["HTTP_HOST"]).'|christmas-promo"</code>'."\n"; |
| 1645 | 1653 | |
| 1646 | 1654 | echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ? |
| 1647 | 1655 | '<div class="ws-menu-page-hr"></div>'."\n". |
| 1648 | - '<p style="margin:0;"><strong>PHP Code:</strong> It is also possible to use PHP tags—optional (for developers). If you use PHP tags, please run a test email with <code><?php print_r(get_defined_vars()); ?></code>. This will give you a full list of all PHP variables available to you in this email. The <code>$paypal</code> variable is the most important one. It contains all of the <code>$_POST</code> variables received from PayPal\'s IPN service—or from an s2Member Pro-Form integration (e.g., <code>$paypal["item_number"]</code>, <code>$paypal["item_name"]</code>, etc). Please note that all Replacement Codes will be parsed first, and then any PHP tags that you\'ve included. Also, please remember that emails are sent in plain text format.</p>'."\n" | |
| 1656 | + '<p style="margin:0;"><strong>Advanced Customization:</strong> Further customization is possible with the <code>ws_plugin__s2member_sp_email_sbj</code> and <code>ws_plugin__s2member_sp_email_msg</code> filters from a plugin or theme.</p>'."\n" | |
| 1649 | 1657 | : ''; |
| 1650 | 1658 | echo '</td>'."\n"; |
| 1651 | 1659 | |
| 1652 | 1660 | echo '</tr>'."\n"; |
| @@ -1662,20 +1670,78 @@ | ||
| 1662 | 1670 | if(apply_filters("ws_plugin__s2member_during_paypal_ops_page_during_left_sections_display_eot_behavior", TRUE, get_defined_vars())) |
| 1663 | 1671 | { |
| 1664 | 1672 | do_action("ws_plugin__s2member_during_paypal_ops_page_during_left_sections_before_eot_behavior", get_defined_vars()); |
| 1665 | 1673 | |
| 1666 | - echo '<div class="ws-menu-page-group" title="Automatic EOT Behavior">'."\n"; | |
| 1674 | + //260908.2031 Allow diagnostic/admin-notice links to open this collapsed panel before the shared menu-page JavaScript scrolls to a setting inside it. | |
| 1675 | + echo '<div class="ws-menu-page-group" title="Automatic End-of-Term Behavior"'.((!empty($_GET['s2member-open-panel']) && $_GET['s2member-open-panel'] === 'auto-eot') ? ' default-state="open"' : '').'>'."\n"; | |
| 1667 | 1676 | |
| 1668 | 1677 | echo '<div class="ws-menu-page-section ws-plugin--s2member-eot-behavior-section">'."\n"; |
| 1669 | - echo '<h3>PayPal EOT Behavior (required, please choose)</h3>'."\n"; | |
| 1670 | - echo '<p>EOT = End Of Term. By default, s2Member will demote a paid Member to a Free Subscriber whenever their Subscription term has ended (i.e., expired), been cancelled, refunded, charged back to you, etc. s2Member demotes them to a Free Subscriber, so they will no longer have Member Level Access to your site. However, in some cases, you may prefer to have Customer accounts deleted completely, instead of just being demoted. This is where you choose which method works best for your site. If you don\'t want s2Member to take ANY action at all, you can disable s2Member\'s EOT System temporarily, or even completely. There are also a few other configurable options here, so please read carefully. These options are all very important.</p>'."\n"; | |
| 1671 | - echo '<p><strong>PayPal IPNs:</strong> The PayPal IPN service will notify s2Member whenever a Member\'s payments have been failing, and/or whenever a Member\'s Subscription has expired for any reason. Even refunds & chargeback reversals are supported through the IPN service. For example, if you issue a refund to an unhappy Customer through PayPal, s2Member will eventually be notified, and the account for that Customer will either be demoted to a Free Subscriber, or deleted automatically (based on your configuration). The communication from PayPal → s2Member is seamless.</p>'."\n"; | |
| 1672 | - echo '<p><em><strong>Some Hairy Details:</strong> There might be times whenever you notice that a Member\'s Subscription has been cancelled through PayPal... but, s2Member continues allowing the User access to your site as a paid Member. Please don\'t be confused by this... in 99.9% of these cases, the reason for this is legitimate. s2Member will only remove the User\'s Membership privileges when an EOT (End Of Term) is processed, a refund occurs, a chargeback occurs, or when a cancellation occurs - which would later result in a delayed Auto-EOT by s2Member.</em></p>'."\n"; | |
| 1673 | - echo '<p><em>s2Member will not process an EOT until the User has completely used up the time they paid for. In other words, if a User signs up for a monthly Subscription on Jan 1st, and then cancels their Subscription on Jan 15th; technically, they should still be allowed to access the site for another 15 days, and then on Feb 1st, the time they paid for has completely elapsed. At that time, s2Member will remove their Membership privileges; by either demoting them to a Free Subscriber, or deleting their account from the system (based on your configuration). s2Member also calculates one extra day (24 hours) into its equation, just to make sure access is not removed sooner than a Customer might expect.</em></p>'."\n"; | |
| 1678 | + echo '<h3>PayPal End-of-Term Behavior (required, please choose)</h3>'."\n"; | |
| 1679 | + echo '<p>EOT = End Of Term. By default, s2Member will demote a paid Member to the role selected under Demote To Role whenever their Subscription term has ended (i.e., expired), been cancelled, refunded, charged back to you, etc. s2Member assigns the role selected under Demote To Role; resulting access depends on that role and on any unrelated WordPress roles you choose to preserve. However, in some cases, you may prefer to have Customer accounts moved to Pending Deletion for administrator review, instead of just being demoted. This is where you choose which method works best for your site. If you don\'t want s2Member to take ANY action at all, you can disable s2Member\'s EOT System temporarily, or even completely. There are also a few other configurable options here, so please read carefully. These options are all very important.</p>'."\n"; | |
| 1680 | + echo '<p><strong>PayPal IPNs:</strong> The PayPal IPN service will notify s2Member whenever a Member\'s payments have been failing, and/or whenever a Member\'s Subscription has expired for any reason. Even refunds & chargeback reversals are supported through the IPN service. For example, if you issue a refund to an unhappy Customer through PayPal, s2Member will eventually be notified, and the account for that Customer will either be assigned the role selected under Demote To Role, or have the configured Delete behavior applied automatically. The communication from PayPal → s2Member is seamless.</p>'."\n"; | |
| 1681 | + echo '<p><em><strong>Some Hairy Details:</strong> There might be times whenever you notice that a Member\'s Subscription has been cancelled through PayPal... but, s2Member continues allowing the User access to your site as a paid Member. Please don\'t be confused by this... in 99.9% of these cases, the reason for this is legitimate. s2Member will only process the membership EOT when an EOT (End Of Term) is reached, a refund occurs, a chargeback occurs, or when a cancellation later results in a delayed Auto-EOT by s2Member.</em></p>'."\n"; | |
| 1682 | + echo '<p><em>s2Member will not process an EOT until the User has completely used up the time they paid for. In other words, if a User signs up for a monthly Subscription on Jan 1st, and then cancels their Subscription on Jan 15th; technically, they should still be allowed to access the site for another 15 days, and then on Feb 1st, the time they paid for has completely elapsed. At that time, s2Member will process their membership EOT by either assigning the role selected under Demote To Role or applying the configured Delete behavior. s2Member also calculates one extra day (24 hours) into its equation, just to make sure access is not removed sooner than a Customer might expect.</em></p>'."\n"; | |
| 1674 | 1683 | do_action("ws_plugin__s2member_during_paypal_ops_page_during_left_sections_during_eot_behavior", get_defined_vars()); |
| 1675 | 1684 | |
| 1676 | - echo '<p id="ws-plugin--s2member-auto-eot-system-enabled-via-cron"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_enabled"] == 2 && (!function_exists("wp_cron") || !wp_get_schedule("ws_plugin__s2member_auto_eot_system__schedule"))) ? '' : ' style="display:none;"').'>If you\'d like to run s2Member\'s Auto-EOT System through a more traditional Cron Job; instead of through <code>WP-Cron</code>, you will need to configure a Cron Job through your server control panel; provided by your hosting company. Set the Cron Job to run <code>once about every 10 minutes to an hour</code>. You\'ll want to configure an HTTP Cron Job that loads this URL:<br /><code>'.esc_html(home_url("/?s2member_auto_eot_system_via_cron=1")).'</code></p>'."\n"; | |
| 1685 | + //260820.0306 Show current processing health where Auto-EOT is configured, so pending/stalled work is visible before it becomes a support issue. | |
| 1686 | + $auto_eot_health = c_ws_plugin__s2member_auto_eots::auto_eot_system_health(TRUE); | |
| 1687 | + $auto_eot_legacy_cap = c_ws_plugin__s2member_auto_eots::auto_eot_system_legacy_cap_info(); | |
| 1688 | + $auto_eot_mode = (string)$GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_enabled"]; | |
| 1689 | + $auto_eot_runtime_target = c_ws_plugin__s2member_auto_eots::auto_eot_system_runtime_budget($auto_eot_mode === '2'); | |
| 1690 | + $php_execution_limit = (int)ini_get('max_execution_time'); | |
| 1691 | + $auto_eot_status_labels = array('healthy' => 'Healthy', 'processing' => 'Processing', 'catching_up' => 'Catching up', 'attention' => 'Attention', 'error' => 'Needs attention', 'disabled' => 'Disabled'); | |
| 1692 | + $auto_eot_status_label = isset($auto_eot_status_labels[$auto_eot_health['status']]) ? $auto_eot_status_labels[$auto_eot_health['status']] : ucfirst($auto_eot_health['status']); | |
| 1677 | 1693 | |
| 1694 | + $auto_eot_next_run = 0; | |
| 1695 | + if($auto_eot_mode === '1') | |
| 1696 | + { | |
| 1697 | + $_scheduled = array_filter(array((int)$auto_eot_health['recurring_at'], (int)$auto_eot_health['continuation_at'])); | |
| 1698 | + $auto_eot_next_run = $_scheduled ? min($_scheduled) : 0; | |
| 1699 | + unset($_scheduled); | |
| 1700 | + } | |
| 1701 | + | |
| 1702 | + echo '<div class="ws-menu-page-hr"></div>'."\n"; | |
| 1703 | + echo '<h3>Automatic Behavior Status: <span class="ws-plugin--s2member-status-light ws-plugin--s2member-status-light-'.esc_attr($auto_eot_health['status']).'" aria-hidden="true"></span>'.esc_html($auto_eot_status_label).'</h3>'."\n"; | |
| 1704 | + echo '<table class="ws-plugin--s2member-status-table"><tbody>'."\n"; | |
| 1705 | + echo '<tr><th scope="row">Pending overdue EOTs:</th><td>'.number_format_i18n((int)$auto_eot_health['pending_count']).'</td></tr>'."\n"; | |
| 1706 | + echo '<tr><th scope="row">Oldest overdue EOT:</th><td>'.($auto_eot_health['oldest_due_at'] ? esc_html(human_time_diff((int)$auto_eot_health['oldest_due_at'], time()).' ago') : 'None').'</td></tr>'."\n"; | |
| 1707 | + echo '<tr><th scope="row">Last completed run:</th><td>'.($auto_eot_health['last_completed_at'] ? esc_html(human_time_diff((int)$auto_eot_health['last_completed_at'], time()).' ago') : 'Not recorded yet').'</td></tr>'."\n"; | |
| 1708 | + echo '<tr><th scope="row">Last run:</th><td>'.($auto_eot_health['last_completed_at'] ? number_format_i18n((int)$auto_eot_health['last_processed']).' EOT(s) in '.esc_html(number_format_i18n((float)$auto_eot_health['last_runtime'], 2)).' seconds' : 'Not recorded yet').'</td></tr>'."\n"; | |
| 1709 | + echo '<tr><th scope="row">Next run:</th><td>'.($auto_eot_mode === '1' ? ($auto_eot_next_run ? esc_html(($auto_eot_next_run <= time() ? human_time_diff($auto_eot_next_run, time()).' overdue' : 'in '.human_time_diff(time(), $auto_eot_next_run))) : 'Not scheduled') : ($auto_eot_mode === '2' ? 'Controlled by your external cron service' : 'Disabled')).'</td></tr>'."\n"; | |
| 1710 | + echo '<tr><th scope="row">Current runtime target:</th><td>About '.esc_html(number_format_i18n($auto_eot_runtime_target, 0)).' seconds'.($php_execution_limit > 0 ? ' (PHP limit: '.esc_html($php_execution_limit).')' : ' (no set PHP limit)').'</td></tr>'."\n"; | |
| 1711 | + //260828.0231 Keep current EOT work and previous EOT history separate while retaining WordPress' native Users table, filters, bulk actions, and Screen Options. | |
| 1712 | + echo '<tr><th scope="row">End-of-Term users:</th><td><a href="'.esc_url(admin_url('/users.php?s2member_view=eot_current')).'">Current</a> | <a href="'.esc_url(admin_url('/users.php?s2member_view=eot_previous')).'">Previous</a></td></tr>'."\n"; | |
| 1713 | + echo '</tbody></table>'."\n"; | |
| 1714 | + echo '<div class="ws-menu-page-hr"></div>'."\n"; | |
| 1715 | + | |
| 1716 | + //260820.0306 Legacy filters remain authoritative for compatibility, but explain when an inherited fixed-batch limit can now reduce adaptive throughput. | |
| 1717 | + if(!empty($auto_eot_legacy_cap['detected'])) | |
| 1718 | + { | |
| 1719 | + echo '<div class="ws-menu-page-notice ws-menu-page-notice-info">'."\n"; | |
| 1720 | + echo '<p><strong>Legacy processing limit detected.</strong> A developer customization is using <code>ws_plugin__s2member_auto_eot_system_per_process</code>. This filter was useful with s2Member\'s older fixed-batch processor, but the new runtime-adaptive engine can usually process more efficiently without it. Unless it exists for a specific site reason, this legacy limit is probably unhelpful now.</p>'."\n"; | |
| 1721 | + | |
| 1722 | + if($auto_eot_legacy_cap['last_hard_cap_source'] === 'filter' && $auto_eot_legacy_cap['last_hard_cap'] !== NULL) | |
| 1723 | + echo '<p>Last effective legacy limit: <strong>'.number_format_i18n((int)$auto_eot_legacy_cap['last_hard_cap']).' EOT(s) per pass</strong>.</p>'."\n"; | |
| 1724 | + | |
| 1725 | + if($auto_eot_legacy_cap['last_hard_cap_source'] === 'filter' && $auto_eot_legacy_cap['last_stop_reason'] === 'legacy_item_cap' && $auto_eot_legacy_cap['estimated_additional'] > 0) | |
| 1726 | + echo '<p>On the last constrained run, s2Member estimates that approximately <strong>'.number_format_i18n((int)$auto_eot_legacy_cap['estimated_additional']).' additional EOT(s)</strong> could have fit within the remaining safe runtime without this hard limit.</p>'."\n"; | |
| 1727 | + | |
| 1728 | + if(!empty($auto_eot_legacy_cap['sources'])) | |
| 1729 | + { | |
| 1730 | + echo '<p><strong>Detected source'.(count($auto_eot_legacy_cap['sources']) === 1 ? '' : 's').':</strong><br />'."\n"; | |
| 1731 | + foreach($auto_eot_legacy_cap['sources'] as $_source) | |
| 1732 | + { | |
| 1733 | + $_source_location = !empty($_source['file']) ? $_source['file'].(!empty($_source['line']) ? ':'.(int)$_source['line'] : '') : (!empty($_source['callback']) ? $_source['callback'] : 'Unknown callback'); | |
| 1734 | + echo '<code>'.esc_html($_source_location).'</code><br />'."\n"; | |
| 1735 | + } | |
| 1736 | + echo '</p>'."\n"; | |
| 1737 | + unset($_source, $_source_location); | |
| 1738 | + } | |
| 1739 | + echo '</div>'."\n"; | |
| 1740 | + } | |
| 1741 | + | |
| 1742 | + echo '<p id="ws-plugin--s2member-auto-eot-system-enabled-via-cron"'.(($auto_eot_mode === '2') ? '' : ' style="display:none;"').'>If you\'d like to run s2Member\'s Auto-EOT System through a traditional Cron Job instead of WP-Cron, configure an HTTP Cron Job through your hosting/server control panel. Running it every <code>1 to 10 minutes</code> works well; about every <code>5 minutes</code> is a good default. Load this URL:<br /><code>'.esc_html(home_url("/?s2member_auto_eot_system_via_cron=1")).'</code></p>'."\n"; | |
| 1743 | + | |
| 1678 | 1744 | echo '<table class="form-table">'."\n"; |
| 1679 | 1745 | echo '<tbody>'."\n"; |
| 1680 | 1746 | echo '<tr>'."\n"; |
| 1681 | 1747 | |
| @@ -1689,15 +1755,15 @@ | ||
| 1689 | 1755 | echo '<tr>'."\n"; |
| 1690 | 1756 | |
| 1691 | 1757 | echo '<td>'."\n"; |
| 1692 | 1758 | echo '<select name="ws_plugin__s2member_auto_eot_system_enabled" id="ws-plugin--s2member-auto-eot-system-enabled">'."\n"; |
| 1693 | - // Very advanced conditionals here. If the Auto-EOT System is NOT running, or NOT fully configured, this will indicate that no option is set - as sort of a built-in acknowledgment/warning in the UI panel. | |
| 1694 | - echo (($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_enabled"] == 1 && (!function_exists("wp_cron") || !wp_get_schedule("ws_plugin__s2member_auto_eot_system__schedule"))) || ($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_enabled"] == 2 && (function_exists("wp_cron") && wp_get_schedule("ws_plugin__s2member_auto_eot_system__schedule"))) || (!$GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_enabled"] && (function_exists("wp_cron") && wp_get_schedule("ws_plugin__s2member_auto_eot_system__schedule")))) ? '<option value=""></option>'."\n" : ''; | |
| 1695 | - echo '<option value="1"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_enabled"] == 1 && function_exists("wp_cron") && wp_get_schedule("ws_plugin__s2member_auto_eot_system__schedule")) ? ' selected="selected"' : '').'>Yes (enable the Auto-EOT System through WP-Cron)</option>'."\n"; | |
| 1696 | - echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ? '<option value="2"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_enabled"] == 2 && (!function_exists("wp_cron") || !wp_get_schedule("ws_plugin__s2member_auto_eot_system__schedule"))) ? ' selected="selected"' : '').'>Yes (but, I\'ll run it with my own Cron Job)</option>'."\n" : ''; | |
| 1697 | - echo '<option value="0"'.((!$GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_enabled"] && (!function_exists("wp_cron") || !wp_get_schedule("ws_plugin__s2member_auto_eot_system__schedule"))) ? ' selected="selected"' : '').'>No (disable the Auto-EOT System)</option>'."\n"; | |
| 1759 | + //260820.0306 Health warnings now report scheduler problems explicitly, so always show the saved mode instead of blanking this field when cron is unhealthy. | |
| 1760 | + echo '<option value="1"'.(($auto_eot_mode === '1') ? ' selected="selected"' : '').'>Yes (enable the Auto-EOT System through WP-Cron)</option>'."\n"; | |
| 1761 | + echo (!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) ? '<option value="2"'.(($auto_eot_mode === '2') ? ' selected="selected"' : '').'>Yes (but, I\'ll run it with my own Cron Job)</option>'."\n" : ''; | |
| 1762 | + echo '<option value="0"'.(($auto_eot_mode === '0') ? ' selected="selected"' : '').'>No (disable the Auto-EOT System)</option>'."\n"; | |
| 1698 | 1763 | echo '</select><br />'."\n"; |
| 1699 | 1764 | echo 'Recommended setting: (<code>Yes / enable via WP-Cron</code>)'."\n"; |
| 1765 | + echo '<br /><em>When disabled, automatic End-of-Term processing is paused. s2Member can still record End-of-Term dates, and s2Member Pro reminders based on stored End-of-Term dates can continue independently. Overdue End-of-Term actions remain pending and will be processed if the Auto-EOT System is enabled again.</em>'."\n"; | |
| 1700 | 1766 | echo '</td>'."\n"; |
| 1701 | 1767 | |
| 1702 | 1768 | echo '</tr>'."\n"; |
| 1703 | 1769 | echo '</tbody>'."\n"; |
| @@ -1707,12 +1773,40 @@ | ||
| 1707 | 1773 | |
| 1708 | 1774 | echo '<table class="form-table">'."\n"; |
| 1709 | 1775 | echo '<tbody>'."\n"; |
| 1710 | 1776 | echo '<tr>'."\n"; |
| 1777 | + echo '<th><label for="ws-plugin--s2member-auto-eot-system-runtime-mode">Auto-EOT Processing Runtime</label></th>'."\n"; | |
| 1778 | + echo '</tr>'."\n"; | |
| 1779 | + echo '<tr>'."\n"; | |
| 1780 | + echo '<td>'."\n"; | |
| 1781 | + echo '<select name="ws_plugin__s2member_auto_eot_system_runtime_mode" id="ws-plugin--s2member-auto-eot-system-runtime-mode">'."\n"; | |
| 1782 | + echo '<option value="auto"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_runtime_mode"] === 'auto') ? ' selected="selected"' : '').'>Automatic (recommended)</option>'."\n"; | |
| 1783 | + echo '<option value="custom"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_runtime_mode"] === 'custom') ? ' selected="selected"' : '').'>Custom maximum runtime</option>'."\n"; | |
| 1784 | + echo '</select><br />'."\n"; | |
| 1785 | + echo '<em>Automatic mode adapts the number of EOTs processed to the wall-clock time available in each run. The current target for this configuration is approximately <strong>'.esc_html(number_format_i18n($auto_eot_runtime_target, 1)).' seconds</strong>; it automatically leaves additional headroom below a finite PHP execution limit.</em>'."\n"; | |
| 1786 | + echo '</td>'."\n"; | |
| 1787 | + echo '</tr>'."\n"; | |
| 1788 | + echo '<tr>'."\n"; | |
| 1789 | + echo '<th><label for="ws-plugin--s2member-auto-eot-system-runtime-custom">Custom Maximum Runtime (seconds)</label></th>'."\n"; | |
| 1790 | + echo '</tr>'."\n"; | |
| 1791 | + echo '<tr>'."\n"; | |
| 1792 | + echo '<td>'."\n"; | |
| 1793 | + echo '<input type="number" min="1" max="3600" step="1" name="ws_plugin__s2member_auto_eot_system_runtime_custom" id="ws-plugin--s2member-auto-eot-system-runtime-custom" value="'.esc_attr($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["auto_eot_system_runtime_custom"]).'" /><br />'."\n"; | |
| 1794 | + echo '<em>Used only in Custom mode. If PHP reports a finite execution limit, s2Member uses the lower of this value and 90% of PHP\'s limit. For example, if you enter 100 seconds and PHP\'s limit is 30 seconds, s2Member uses 27 seconds. Developers can still override the final runtime with <code>ws_plugin__s2member_auto_eot_system_runtime</code>.</em>'."\n"; | |
| 1795 | + echo '</td>'."\n"; | |
| 1796 | + echo '</tr>'."\n"; | |
| 1797 | + echo '</tbody>'."\n"; | |
| 1798 | + echo '</table>'."\n"; | |
| 1711 | 1799 | |
| 1800 | + echo '<div class="ws-menu-page-hr"></div>'."\n"; | |
| 1801 | + | |
| 1802 | + echo '<table class="form-table">'."\n"; | |
| 1803 | + echo '<tbody>'."\n"; | |
| 1804 | + echo '<tr>'."\n"; | |
| 1805 | + | |
| 1712 | 1806 | echo '<th>'."\n"; |
| 1713 | 1807 | echo '<label for="ws-plugin--s2member-membership-eot-behavior">'."\n"; |
| 1714 | - echo 'Membership EOT Behavior (Demote or Delete)?'."\n"; | |
| 1808 | + echo 'Membership End-of-Term Behavior (Demote or Delete)?'."\n"; | |
| 1715 | 1809 | echo '</label>'."\n"; |
| 1716 | 1810 | echo '</th>'."\n"; |
| 1717 | 1811 | |
| 1718 | 1812 | echo '</tr>'."\n"; |
| @@ -1719,14 +1813,20 @@ | ||
| 1719 | 1813 | echo '<tr>'."\n"; |
| 1720 | 1814 | |
| 1721 | 1815 | echo '<td>'."\n"; |
| 1722 | 1816 | echo '<select name="ws_plugin__s2member_membership_eot_behavior" id="ws-plugin--s2member-membership-eot-behavior">'."\n"; |
| 1723 | - echo '<option value="demote"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["membership_eot_behavior"] === "demote") ? ' selected="selected"' : '').'>Demote (convert them to a Free Subscriber)</option>'."\n"; | |
| 1724 | - echo '<option value="delete"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["membership_eot_behavior"] === "delete") ? ' selected="selected"' : '').'>Delete (erase their account completely)</option>'."\n"; | |
| 1725 | - echo '</select>'."\n"; | |
| 1817 | + echo '<option value="demote"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["membership_eot_behavior"] === "demote") ? ' selected="selected"' : '').'>Demote (assign the role selected under Demote To Role)</option>'."\n"; | |
| 1818 | + echo '<option value="delete"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["membership_eot_behavior"] === "delete") ? ' selected="selected"' : '').'>Delete (move to Pending Deletion for review)</option>'."\n"; | |
| 1819 | + echo '</select><br />'."\n"; | |
| 1820 | + //260822.0614 Keep the stored `delete` value for compatibility; irreversible deletion is an explicit developer opt-in rather than the product default. | |
| 1821 | + echo '<em>Delete removes s2Member membership access and moves the account to the <strong>Pending Deletion</strong> role for administrator review. To restore historical automatic irreversible deletion, a developer must explicitly allow it with the <code>ws_plugin__s2member_allow_eot_user_deletion</code> filter.</em>'."\n"; | |
| 1726 | 1822 | echo '</td>'."\n"; |
| 1727 | 1823 | |
| 1728 | 1824 | echo '</tr>'."\n"; |
| 1825 | + | |
| 1826 | + //260916.1840 Keep the global EOT role destination/scope controls identical across every gateway page. | |
| 1827 | + c_ws_plugin__s2member_menu_pages::eot_demotion_options(); | |
| 1828 | + | |
| 1729 | 1829 | echo '<tr>'."\n"; |
| 1730 | 1830 | |
| 1731 | 1831 | echo '<th>'."\n"; |
| 1732 | 1832 | echo '<label for="ws-plugin--s2member-eots-remove-ccaps">'."\n"; |
| @@ -1779,10 +1879,11 @@ | ||
| 1779 | 1879 | echo '<tbody>'."\n"; |
| 1780 | 1880 | echo '<tr>'."\n"; |
| 1781 | 1881 | |
| 1782 | 1882 | echo '<th>'."\n"; |
| 1883 | + //260824.1957 Clarify that disputes/chargebacks are normalized into the existing Reversals policy without changing stored option values. | |
| 1783 | 1884 | echo '<label for="ws-plugin--s2member-triggers-immediate-eot">'."\n"; |
| 1784 | - echo 'Refunds/Partial Refunds/Reversals (trigger Immediate EOT)?'."\n"; | |
| 1885 | + echo 'Refunds/Partial Refunds/Reversals/Disputes (trigger Immediate EOT)?'."\n"; | |
| 1785 | 1886 | echo '</label>'."\n"; |
| 1786 | 1887 | echo '</th>'."\n"; |
| 1787 | 1888 | |
| 1788 | 1889 | echo '</tr>'."\n"; |
| @@ -1791,13 +1892,13 @@ | ||
| 1791 | 1892 | echo '<td>'."\n"; |
| 1792 | 1893 | echo '<select name="ws_plugin__s2member_triggers_immediate_eot" id="ws-plugin--s2member-triggers-immediate-eot">'."\n"; |
| 1793 | 1894 | echo '<option value="none"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["triggers_immediate_eot"] === "none") ? ' selected="selected"' : '').'>None (I\'ll review these events manually)</option>'."\n"; |
| 1794 | 1895 | echo '<option value="refunds"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["triggers_immediate_eot"] === "refunds") ? ' selected="selected"' : '').'>Full Refunds (full refunds only; ALWAYS trigger an Immediate EOT action)</option>'."\n"; |
| 1795 | - echo '<option value="reversals"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["triggers_immediate_eot"] === "reversals") ? ' selected="selected"' : '').'>Reversals (chargebacks only; ALWAYS trigger an Immediate EOT action)</option>'."\n"; | |
| 1796 | - echo '<option value="refunds,reversals"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["triggers_immediate_eot"] === "refunds,reversals") ? ' selected="selected"' : '').'>Full Refunds, Reversals (these ALWAYS trigger an Immediate EOT action)</option>'."\n"; | |
| 1797 | - echo '<option value="refunds,partial_refunds,reversals"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["triggers_immediate_eot"] === "refunds,partial_refunds,reversals") ? ' selected="selected"' : '').'>Full Refunds, Partial Refunds, Reversals (these ALWAYS trigger an Immediate EOT action)</option>'."\n"; | |
| 1896 | + echo '<option value="reversals"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["triggers_immediate_eot"] === "reversals") ? ' selected="selected"' : '').'>Reversals/Disputes (including chargebacks; ALWAYS trigger an Immediate EOT action)</option>'."\n"; | |
| 1897 | + echo '<option value="refunds,reversals"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["triggers_immediate_eot"] === "refunds,reversals") ? ' selected="selected"' : '').'>Full Refunds, Reversals/Disputes (these ALWAYS trigger an Immediate EOT action)</option>'."\n"; | |
| 1898 | + echo '<option value="refunds,partial_refunds,reversals"'.(($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["triggers_immediate_eot"] === "refunds,partial_refunds,reversals") ? ' selected="selected"' : '').'>Full Refunds, Partial Refunds, Reversals/Disputes (these ALWAYS trigger an Immediate EOT action)</option>'."\n"; | |
| 1798 | 1899 | echo '</select><br />'."\n"; |
| 1799 | - echo '<em><strong>Note:</strong> s2Member is not equipped to detect partial refunds against multi-payment Subscriptions reliably. Therefore, all refunds processed against Subscriptions (of any kind) are considered <strong>Partial</strong> Refunds. Full refunds (in the eyes of s2Member) occur only against Buy Now transactions where it is easy for s2Member to see that the refund amount is >= the original Buy Now purchase price (i.e., a Full Refund). <strong>Also Note:</strong> This setting (no matter what you choose) will NOT impact s2Member\'s internal API Notifications for Refund/Reversal events. <a href="#" onclick="alert(\'A Full or Partial Refund; and/or a Reversal Notification will ALWAYS be processed internally by s2Member, even if no action is taken by s2Member in accordance with your configuration here.\\n\\nIn this way, you\\\'ll have the full ability to listen for these events on your own (via API Notifications); if you prefer (optional). For more information, check your Dashboard under: `s2Member → API Notifications → Refunds/Reversals`.\'); return false;">Click here for details</a>.</em>'."\n"; | |
| 1900 | + echo '<em><strong>Disputes/Chargebacks:</strong> When a supported gateway reports a dispute or chargeback, s2Member treats it as a Reversal for this setting. <strong>Note:</strong> s2Member is not equipped to detect partial refunds against multi-payment Subscriptions reliably. Therefore, all refunds processed against Subscriptions (of any kind) are considered <strong>Partial</strong> Refunds. Full refunds (in the eyes of s2Member) occur only against Buy Now transactions where it is easy for s2Member to see that the refund amount is >= the original Buy Now purchase price (i.e., a Full Refund). <strong>Also Note:</strong> This setting (no matter what you choose) will NOT impact s2Member\'s internal API Notifications for Refund/Reversal events. <a href="#" onclick="alert(\'A Full or Partial Refund; and/or a Reversal Notification will ALWAYS be processed internally by s2Member, even if no action is taken by s2Member in accordance with your configuration here.\\n\\nIn this way, you\\\'ll have the full ability to listen for these events on your own (via API Notifications); if you prefer (optional). For more information, check your Dashboard under: `s2Member → API Notifications → Refunds/Reversals`.\'); return false;">Click here for details</a>.</em>'."\n"; | |
| 1800 | 1901 | echo '</td>'."\n"; |
| 1801 | 1902 | |
| 1802 | 1903 | echo '</tr>'."\n"; |
| 1803 | 1904 | echo '</tbody>'."\n"; |