# s2member/261001/src/includes/classes/paypal-checkout-in.inc.php

s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls &amp; Member Access Subscriptions, version 261001. 1,474 lines.

- Page: https://pluginprobe.com/plugins/s2member/261001/code/src/includes/classes/paypal-checkout-in.inc.php
- Raw: https://pluginprobe.com/plugins/s2member/261001/raw/src/includes/classes/paypal-checkout-in.inc.php
- Modified: 2026-10-01T05:36:18+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/s2member/261001/code/src/includes/classes/paypal-checkout-in.inc.php#L10-L20`.

```php
<?php
// @codingStandardsIgnoreFile
/**
 * s2Member's PayPal Checkout (REST) handler.
 *
 * Server-side entrypoint for PayPal Checkout operations used by s2Member shortcodes:
 * - Buy Now: create_order + capture_order (one-time payments).
 * - Subscriptions (membership level): create_subscription/get_plan_id + confirm_subscription.
 * - output="url|anchor": redirect/return flow (does not create orders on page load).
 * - Optional: cancel_subscription (on-site cancel for logged-in users).
 *
 * Successful operations are proxied into s2Member's existing PayPal notify/return handlers,
 * preserving legacy provisioning behavior (level/ccaps/EOT/etc.) without rewriting it.
 *
 * @package s2Member\PayPal
 * @since 260101
 */
if(!defined('WPINC')) // MUST have WordPress.
	exit ('Do not access this file directly.');

if(!class_exists('c_ws_plugin__s2member_paypal_checkout_in'))
{
	class c_ws_plugin__s2member_paypal_checkout_in
	{
		public static function paypal_checkout()
		{
			if(empty($_REQUEST['s2member_paypal_checkout']))
				return;

			@set_time_limit(0);
			@ini_set('memory_limit', apply_filters('admin_memory_limit', WP_MAX_MEMORY_LIMIT));
			@ini_set('display_errors', '0');

			$op = !empty($_REQUEST['s2member_paypal_checkout_op']) ? strtolower(trim(stripslashes((string)$_REQUEST['s2member_paypal_checkout_op']))) : '';
			$t  = !empty($_REQUEST['s2member_paypal_checkout_t'])  ? trim(stripslashes((string)$_REQUEST['s2member_paypal_checkout_t'])) : '';

			$is_redirect_mode = in_array($op, array('redirect', 'return', 'cancel'), true);

			$env_setting = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_sandbox() ? 'sandbox' : 'live';

			if(!headers_sent())
			{
				nocache_headers();
				if($is_redirect_mode)
					header('Content-Type: text/html; charset=UTF-8');
				else
					header('Content-Type: application/json; charset=UTF-8');
			}

			c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
					'ppco'    => 'checkout',
					'env_setting' => $env_setting,
					'event'   => 'request',
					'get'     => $_GET,
					'post'    => $_POST,
					'method'  => !empty($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '',
					'ip'      => !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
					'ua'      => !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
					'referer' => !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '',
				));

			if(!$op || !$t)
			{
				echo wp_json_encode(array('error' => 'missing_op_or_token'));
				exit();
			}
			$raw = c_ws_plugin__s2member_utils_encryption::decrypt($t);

			//260808 Safely unserialize the PayPal checkout token.
			$token = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($raw);

			if(!is_array($token))
				$token = false;

			if(!$token || !is_array($token))
			{
				echo wp_json_encode(array('error' => 'invalid_token'));
				exit();
			}
			//260928.1645 Never write a reusable signed Gateway Checkout browser token to PayPal debug logs; the encrypted shortcode token remains available to the handler itself.
			$log_token = $token;
			unset($log_token['gateway_checkout_token']);
			if(!empty($token['exp']) && is_numeric($token['exp']) && time() > (int)$token['exp'])
			{
				echo wp_json_encode(array('error' => 'token_expired'));
				exit();
			}
			if(empty($token['invoice']) || empty($token['ip']) || empty($token['item_number']) || empty($token['checksum']))
			{
				echo wp_json_encode(array('error' => 'token_incomplete'));
				exit();
			}
			if($token['checksum'] !== md5($token['invoice'].$token['ip'].$token['item_number']))
			{
				echo wp_json_encode(array('error' => 'token_checksum_mismatch'));
				exit();
			}

			//260928.1520 Framework button shortcodes use the same durable coordinator as Pro-Forms, initialized before any provider-side create operation and required for later browser return/confirmation.
			if(strpos((string)$token['invoice'], 's2mb-') === 0 && $op !== 'cancel')
			{
				$create_allowed = in_array($op, array('create_order', 'create_subscription', 'get_plan_id', 'redirect'), TRUE);
				$prepared = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_button_gateway_checkout_prepare($token, $create_allowed);
				if(empty($prepared['ok']))
				{
					$error = !empty($prepared['error']) ? (string)$prepared['error'] : 'gateway_checkout_unavailable';
					if($is_redirect_mode)
						echo esc_html($error);
					else
						echo wp_json_encode(array('error' => $error));
					exit();
				}
			}

			if($token['ip'] !== c_ws_plugin__s2member_utils_ip::current())
			{
				//260414 PayPal Checkout browser returns can legitimately arrive with a different client IP; log it, but do not fail the token.
				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'  => 'checkout',
						'env_setting' => $env_setting,
						'event' => 'token_ip_mismatch',
						'token' => $log_token,
						'ip'    => c_ws_plugin__s2member_utils_ip::current(),
					));
			}

			$old__subscr_gateway = !empty($token['old__subscr_gateway']) ? (string)$token['old__subscr_gateway'] : '';
			$old__subscr_id = !empty($token['old__subscr_id']) ? (string)$token['old__subscr_id'] : '';
			$old__subscr_baid = !empty($token['old__subscr_baid']) ? (string)$token['old__subscr_baid'] : '';
			$old__subscr_cid = !empty($token['old__subscr_cid']) ? (string)$token['old__subscr_cid'] : '';
			$old__ipn_signup_vars = (!empty($token['old__ipn_signup_vars']) && is_array($token['old__ipn_signup_vars'])) ? $token['old__ipn_signup_vars'] : array(); //260408 Use the old context captured before the buyer left for PayPal.

			// output="anchor|url" support: redirect-mode endpoints (GET).
			if($op === 'redirect' || $op === 'return' || $op === 'cancel')
			{
				// NOTE: These endpoints are intended for output="anchor|url" shortcode formats.
				// They redirect to PayPal approval URLs, then auto-POST into s2Member's existing PayPal notify + return handlers.

				if($op === 'cancel')
				{
					$cancel = !empty($token['cancel']) ? (string)$token['cancel'] : home_url('/');
					$cancel = wp_validate_redirect($cancel, home_url('/'));
					wp_redirect($cancel);
					exit();
				}

				$endpoint = home_url('/?s2member_paypal_checkout=1');
				$return_url = $endpoint.'&s2member_paypal_checkout_op=return&s2member_paypal_checkout_t='.rawurlencode($t);
				$cancel_url = $endpoint.'&s2member_paypal_checkout_op=cancel&s2member_paypal_checkout_t='.rawurlencode($t);

				if($op === 'redirect')
				{
					$pp_token = $token;
					$pp_token['return'] = $return_url;
					$pp_token['cancel'] = $cancel_url;

					if((!isset($pp_token['rr']) || (string)$pp_token['rr'] === '') || strtoupper((string)$pp_token['rr']) === 'BN')
					{
						$order = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_create($pp_token);

						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
							'ppco'   => 'checkout',
							'env_setting' => $env_setting,
							'event'  => 'redirect_order_create_response',
							'order'  => $order,
							'token'  => $log_token,
						));

						$approve_url = '';
						if(!empty($order['links']) && is_array($order['links']))
							foreach($order['links'] as $link)
								if(!empty($link['rel']) && !empty($link['href']))
								{
									$rel = strtolower((string)$link['rel']);
									if($rel === 'approve' || $rel === 'payer-action' || $rel === 'approval_url')
										$approve_url = (string)$link['href'];
								}

						//260928.1605 A resumed Gateway Checkout returns its existing provider ID, not the original create response links; fetch the provider resource rather than create another chargeable order.
						if(!$approve_url && !empty($order['id']) && strpos((string)$token['invoice'], 's2mb-') === 0)
						{
							$order_details = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_details((string)$order['id']);
							if(empty($order_details['__error']) && !empty($order_details['links']) && is_array($order_details['links']))
								foreach($order_details['links'] as $link)
									if(!empty($link['rel']) && !empty($link['href']) && in_array(strtolower((string)$link['rel']), array('approve', 'payer-action', 'approval_url'), TRUE))
										$approve_url = (string)$link['href'];
						}

						if(!$approve_url)
						{
							echo 'order_approval_url_missing';
							exit();
						}

						wp_redirect($approve_url);
						exit();
					}
					else
					{
						$subscription = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_create($pp_token);

						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
							'ppco'         => 'checkout',
							'env_setting' => $env_setting,
							'event'        => 'redirect_subscription_create_response',
							'subscription' => $subscription,
							'token'        => $log_token,
						));

						$approve_url = '';
						if(!empty($subscription['links']) && is_array($subscription['links']))
							foreach($subscription['links'] as $link)
								if(!empty($link['rel']) && !empty($link['href']) && strtolower((string)$link['rel']) === 'approve')
									$approve_url = (string)$link['href'];

						//260928.1605 Reuse the same persisted PayPal subscription on repeated redirect clicks. A details GET may supply the approval link without starting a second subscription.
						if(!$approve_url && !empty($subscription['id']) && strpos((string)$token['invoice'], 's2mb-') === 0)
						{
							$subscription_details = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_details((string)$subscription['id']);
							if(empty($subscription_details['__error']) && !empty($subscription_details['links']) && is_array($subscription_details['links']))
								foreach($subscription_details['links'] as $link)
									if(!empty($link['rel']) && !empty($link['href']) && strtolower((string)$link['rel']) === 'approve')
										$approve_url = (string)$link['href'];
						}

						if(!$approve_url)
						{
							echo 'subscription_approval_url_missing';
							exit();
						}

						wp_redirect($approve_url);
						exit();
					}
				}

				// Return URL: PayPal redirects here after approval.
				if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
				{
					$order_id = !empty($_GET['token']) ? trim(stripslashes((string)$_GET['token'])) : '';
					if(!$order_id)
					{
						echo 'missing_order_id';
						exit();
					}

					$capture = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_capture($order_id, $token);

					$cap0 = (!empty($capture['purchase_units'][0]['payments']['captures'][0]) && is_array($capture['purchase_units'][0]['payments']['captures'][0])) ? $capture['purchase_units'][0]['payments']['captures'][0] : array();
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'       => 'checkout',
						'env_setting' => $env_setting,
						'event'      => 'capture_response',
						'order_id'   => $order_id,
						'status'     => !empty($capture['status']) ? (string)$capture['status'] : '',
						'capture_id' => !empty($cap0['id']) ? (string)$cap0['id'] : '',
						'amount'     => !empty($cap0['amount']['value']) ? (string)$cap0['amount']['value'] : '',
						'cc'         => !empty($cap0['amount']['currency_code']) ? (string)$cap0['amount']['currency_code'] : '',
						'payer'      => !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '',
						'capture'    => $capture,
						'token'      => $log_token,
					));

					if(!empty($capture['__error']))
					{
						echo (string)$capture['__error'];
						exit();
					}

					if(empty($capture['status']) || strtoupper($capture['status']) !== 'COMPLETED')
					{
						echo 'order_capture_failed';
						exit();
					}

					//260928.1538 Framework button redirect purchases use the shared coordinator fulfillment instead of a second hand-written notify/return path.
					if(strpos((string)$token['invoice'], 's2mb-') === 0)
					{
						$fulfillment = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_fulfill($capture, $token);
						if(empty($fulfillment['ok']) || empty($fulfillment['rtn_url']) || empty($fulfillment['rtn_post']))
						{
							echo esc_html(!empty($fulfillment['error']) ? (string)$fulfillment['error'] : 'order_fulfillment_failed');
							exit();
						}
						echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /></head><body>';
						echo '<form id="s2m_ppco_rtn" method="post" accept-charset="UTF-8" action="'.esc_attr($fulfillment['rtn_url']).'">';
						foreach($fulfillment['rtn_post'] as $k => $v)
							echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr((string)$v).'" />';
						echo '</form><script type="text/javascript">document.getElementById("s2m_ppco_rtn").submit();</script></body></html>';
						exit();
					}

					//260818.0126 Keep submitted Pro-Form contact details for pro-emails; they may differ from the payer's PayPal profile.
					$is_pro_form = (!empty($token['s2member_paypal_proxy_use']) && (string)$token['s2member_paypal_proxy_use'] === 'pro-emails');
					$payer_email = ($is_pro_form && isset($token['payer_email'])) ? sanitize_email((string)$token['payer_email']) : (!empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '');
					$first_name  = ($is_pro_form && isset($token['first_name'])) ? (string)$token['first_name'] : (!empty($capture['payer']['name']['given_name']) ? (string)$capture['payer']['name']['given_name'] : '');
					$last_name   = ($is_pro_form && isset($token['last_name'])) ? (string)$token['last_name'] : (!empty($capture['payer']['name']['surname']) ? (string)$capture['payer']['name']['surname'] : '');

					$pu_amount   = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['value']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['value'] : '';
					$pu_cc       = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code'] : '';
					$pu_cap_id   = !empty($capture['purchase_units'][0]['payments']['captures'][0]['id']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['id'] : '';

					if(!$payer_email || !$pu_amount || !$pu_cc || !$pu_cap_id)
					{
						echo 'capture_missing_fields';
						exit();
					}

					//260228 Normalize amount strings before comparison (e.g. 20 vs 20.00).
					if(!empty($token['amount']) && number_format((float)$token['amount'], 2, '.', '') !== number_format((float)$pu_amount, 2, '.', ''))
					{
						echo 'amount_mismatch';
						exit();
					}
					if(!empty($token['cc']) && strtoupper((string)$token['cc']) !== strtoupper((string)$pu_cc))
					{
						echo 'currency_mismatch';
						exit();
					}

					$paypal = array(
						'txn_type'       => 'web_accept',
						'payment_status' => 'Completed',
						'txn_id'         => $pu_cap_id,
						'mc_gross'       => $pu_amount,
						'mc_currency'    => $pu_cc,
						'invoice'        => (string)$token['invoice'],
						'custom'         => (string)$token['custom'],
						'item_name'      => (string)$token['item_name'],
						'item_number'    => (string)$token['item_number'],
						'option_name1'      => (string)$token['on0'],
						'option_selection1' => (string)$token['os0'],
						'option_name2'      => (string)$token['on1'],
						'option_selection2' => (string)$token['os1'],
						'payer_email'    => $payer_email,
						'first_name'     => $first_name,
						'last_name'      => $last_name,
					);

					//260817.2119 Preserve Pro-Form tax in the simulated IPN so existing fulfillment and email logic receives the same calculated values as the legacy Pro flow.
					if(isset($token['tax']))
						$paypal['tax'] = (string)$token['tax'];

					$is_independent_ccaps_sale = (strpos((string)$token['item_number'], '*:') === 0);
					$is_specific_post_page_sale = (strpos((string)$token['item_number'], 'sp:') === 0);
					$can_cancel_old_subscr = (!$is_independent_ccaps_sale && !$is_specific_post_page_sale); //260407 Only membership replacement-style PPCO purchases should cancel an existing recurring subscription here.

					//260817.2119 Keep normal Checkout defaults while allowing an encrypted Pro-Form token to request its existing email, coupon, and success-URL handling during the internal Notify call.
					$proxy_use = !empty($token['s2member_paypal_proxy_use']) ? (string)$token['s2member_paypal_proxy_use'] : 'paypal_checkout';
					$notify_extra = array();

					if(!empty($token['s2member_paypal_proxy_coupon']) && is_array($token['s2member_paypal_proxy_coupon']))
						$notify_extra['s2member_paypal_proxy_coupon'] = $token['s2member_paypal_proxy_coupon'];
					if(array_key_exists('s2member_paypal_proxy_return_url', $token))
						$notify_extra['s2member_paypal_proxy_return_url'] = (string)$token['s2member_paypal_proxy_return_url'];

					$notify_done_option = 's2m_ppco_capture_done_'.md5($pu_cap_id);
					$notify_result = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_notify_once($paypal, $notify_done_option, $proxy_use, $notify_extra);

					if(empty($notify_result['ok']))
					{
						if($is_redirect_mode)
							echo !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed';
						else
						{
							if(!headers_sent())
								status_header(500);

							echo wp_json_encode(array('error' => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed'));
						}
						exit();
					}

					//260817 Only the request that actually performed fulfillment should trigger replacement-subscription cancellation.
					if(!empty($notify_result['processed']) && $can_cancel_old_subscr && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $pu_cap_id), get_defined_vars())) //260406
						c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); //260407

					$return_url = (string)$token['return'];
					$return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url);

					$return_post = array_merge($paypal, array(
						's2member_paypal_proxy'     => 'paypal',
						's2member_paypal_proxy_use' => $proxy_use,
					));

					//260817 Carry the already-resolved Pro-Form success URL inside the signed browser-return package.
					if(array_key_exists('s2member_paypal_proxy_return_url', $token))
						$return_post['s2member_paypal_proxy_return_url'] = !empty($notify_result['body']) ? trim((string)$notify_result['body']) : '';

					//260817 Sign the exact browser-return payload without exposing the reusable internal PayPal proxy key.
					$return_handoff = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_return_handoff_create($return_post);
					if(!$return_handoff)
					{
						echo 'return_handoff_failed';
						exit();
					}
					$return_post['s2member_paypal_checkout_handoff'] = $return_handoff;

					// Auto-POST into s2Member's existing PayPal return handler.
					echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /></head><body>';
					echo '<form id="s2m_ppco_rtn" method="post" accept-charset="UTF-8" action="'.esc_attr($return_url).'">'; //260817 Keep the signed browser-return payload encoding stable.
					foreach($return_post as $k => $v)
						echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr((string)$v).'" />';
					echo '</form><script type="text/javascript">document.getElementById("s2m_ppco_rtn").submit();</script></body></html>';
					exit();
				}
				else
				{
					$subscription_id = !empty($_GET['subscription_id']) ? trim(stripslashes((string)$_GET['subscription_id'])) : '';
					if(!$subscription_id)
					{
						echo 'missing_subscription_id';
						exit();
					}

					$subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscription_id));

					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting' => $env_setting,
						'event'           => 'subscription_get_response',
						'subscription_id' => $subscription_id,
						'code'            => !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0,
						'body'            => !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '',
						'token'           => $log_token,
					));

					$subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0;
					$subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '';

					$subscription = array();
					if($subscription_body)
						$subscription = json_decode($subscription_body, true);

					if(!is_array($subscription))
						$subscription = array();

					if($subscription_code < 200 || $subscription_code > 299 || empty($subscription['id']))
					{
						echo 'subscription_get_failed';
						exit();
					}

					$custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : '';
					if($custom_id && (string)$token['invoice'] && $custom_id !== (string)$token['invoice'])
					{
						echo 'subscription_custom_id_mismatch';
						exit();
					}

					//260928.1538 A returned Framework button and an ACTIVATED webhook resolve the same provider subscription against the same saved purchase token.
					if(strpos((string)$token['invoice'], 's2mb-') === 0)
					{
						$fulfillment = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_button_subscription_fulfill($subscription, $token, 'return');
						if(!empty($fulfillment['pending_activation']))
						{
							//260928.1703 PayPal can redirect before ACTIVE (or while the webhook holds the checkout lock). Recheck the same signed return, without creating another subscription or showing a false payment failure.
							$wait_attempt = isset($_GET['s2member_paypal_checkout_wait']) ? max(0, (int)$_GET['s2member_paypal_checkout_wait']) : 0;
							echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /><title>PayPal subscription confirmation</title></head><body>';
							echo '<p>PayPal is confirming your subscription. Please do not start a second checkout.</p>';
							if($wait_attempt < 12)
							{
								$poll_url = add_query_arg(array('subscription_id' => $subscription_id, 's2member_paypal_checkout_wait' => $wait_attempt + 1), $return_url);
								echo '<script type="text/javascript">setTimeout(function(){window.location.replace('.wp_json_encode($poll_url, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT).');},2000);</script>';
							}
							else
								echo '<p>Confirmation is taking longer than expected. If PayPal activates the subscription, s2Member will complete it through the webhook; check your registration email before trying again.</p>';
							echo '</body></html>';
							exit();
						}
						if(empty($fulfillment['ok']) || empty($fulfillment['rtn_url']) || empty($fulfillment['rtn_post']))
						{
							echo esc_html(!empty($fulfillment['error']) ? (string)$fulfillment['error'] : 'subscription_fulfillment_failed');
							exit();
						}
						echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /></head><body>';
						echo '<form id="s2m_ppco_rtn" method="post" accept-charset="UTF-8" action="'.esc_attr($fulfillment['rtn_url']).'">';
						foreach($fulfillment['rtn_post'] as $k => $v)
							echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr((string)$v).'" />';
						echo '</form><script type="text/javascript">document.getElementById("s2m_ppco_rtn").submit();</script></body></html>';
						exit();
					}

					$subscriber_email = !empty($subscription['subscriber']['email_address']) ? (string)$subscription['subscriber']['email_address'] : '';
					$first_name = !empty($subscription['subscriber']['name']['given_name']) ? (string)$subscription['subscriber']['name']['given_name'] : '';
					$last_name  = !empty($subscription['subscriber']['name']['surname']) ? (string)$subscription['subscriber']['name']['surname'] : '';

					$paypal = array(
						'txn_type'       => 'subscr_signup',
						'payment_status' => 'Completed',
						'subscr_gateway' => 'paypal',

						'txn_id'         => $subscription_id,
						'subscr_id'      => $subscription_id,
						'subscr_baid'    => $subscription_id,
						'subscr_cid'     => $subscription_id,

						'mc_gross'       => (string)$token['amount'],
						'mc_currency'    => strtoupper((string)$token['cc']),

						'period1'        => (!empty($token['tp']) && !empty($token['tt'])) ? ((string)$token['tp'].' '.strtoupper((string)$token['tt'])) : '0 D',
						'mc_amount1'     => (!empty($token['tp']) && !empty($token['tt'])) ? (string)$token['ta'] : '0.00',

						'period3'        => ((string)$token['rp'].' '.strtoupper((string)$token['rt'])),
						'mc_amount3'     => (string)$token['amount'],
						'recurring'      => ((isset($token['rr']) && (string)$token['rr'] === '1') ? '1' : '0'),

						'invoice'        => (string)$token['invoice'],
						'custom'         => (string)$token['custom'],
						'item_name'      => (string)$token['item_name'],
						'item_number'    => (string)$token['item_number'],

						'payer_email'    => $subscriber_email,
						'first_name'     => $first_name,
						'last_name'      => $last_name,

						'option_name1'      => (string)$token['on0'],
						'option_selection1' => (string)$token['os0'],
						'option_name2'      => (string)$token['on1'],
						'option_selection2' => (string)$token['os1'],
					);

					$option_ppco_subscr = 's2m_ppco_subscr_done_'.md5($subscription_id);

					//260818.0603 Share the success-only Notify lock/done marker with browser confirmation and webhook activation fallback.
					$notify_result = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_notify_once($paypal, $option_ppco_subscr);

					if(empty($notify_result['ok']))
					{
						echo !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed';
						exit();
					}

					//260818.0603 Only the request that completed Notify should cancel a replaced subscription; duplicates are already fulfilled.
					if(!empty($notify_result['processed']) && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $subscription_id), get_defined_vars()))
						c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars);

					$return_url2 = (string)$token['return'];
					$return_url2 = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url2);

					$return_post2 = array_merge($paypal, array(
						's2member_paypal_proxy'     => 'paypal',
						's2member_paypal_proxy_use' => 'paypal_checkout',
					));

					//260817 Sign the exact browser-return payload without exposing the reusable internal PayPal proxy key.
					$return_handoff = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_return_handoff_create($return_post2);
					if(!$return_handoff)
					{
						echo 'return_handoff_failed';
						exit();
					}
					$return_post2['s2member_paypal_checkout_handoff'] = $return_handoff;

					echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /></head><body>';
					echo '<form id="s2m_ppco_rtn" method="post" accept-charset="UTF-8" action="'.esc_attr($return_url2).'">'; //260817 Keep the signed browser-return payload encoding stable.
					foreach($return_post2 as $k => $v)
						echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr((string)$v).'" />';
					echo '</form><script type="text/javascript">document.getElementById("s2m_ppco_rtn").submit();</script></body></html>';
					exit();
				}
			}

			if($op === 'create_subscription')
			{
				if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
				{
					echo wp_json_encode(array('error' => 'not_subscription'));
					exit();
				}

				$subscription = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_create($token);

				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
					'ppco'         => 'checkout',
					'env_setting'  => $env_setting,
					'event'        => 'create_subscription_response',
					'subscription' => $subscription,
					'token'        => $log_token,
				));

				if(empty($subscription['id']))
				{
					$error = !empty($subscription['__error']) ? (string)$subscription['__error'] : 'subscription_create_failed';
					$recoverable = in_array($error, array('subscription_create_unresolved', 'gateway_checkout_busy'), TRUE);
					//260902.0200 Let coordinator-backed browser flows briefly wait for webhook repair only when creation is genuinely unresolved/in progress; deterministic failures remain immediate errors.
					echo wp_json_encode(array('error' => $error, 'recoverable' => $recoverable));
					exit();
				}

				//260901.2145 The browser receives only the already-persisted PayPal subscription ID; PayPal's JS SDK handles buyer approval from that server-created resource.
				echo wp_json_encode(array('subscription_id' => (string)$subscription['id']));
				exit();
			}

			if($op === 'get_subscription_id')
			{
				if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
				{
					echo wp_json_encode(array('error' => 'not_subscription'));
					exit();
				}

				$gateway_checkout_id = !empty($token['gateway_checkout_id']) && c_ws_plugin__s2member_gateway_checkouts::valid_id((string)$token['gateway_checkout_id']) ? (string)$token['gateway_checkout_id'] : '';
				$gateway_checkout = $gateway_checkout_id ? c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id) : FALSE;
				if(!$gateway_checkout || (string)$gateway_checkout['gateway'] !== 'paypal_checkout' || (string)$gateway_checkout['operation'] !== 'subscription')
				{
					echo wp_json_encode(array('error' => 'gateway_checkout_invalid'));
					exit();
				}

				$subscription_id = !empty($gateway_checkout['gateway_ids']['subscription_id']) ? (string)$gateway_checkout['gateway_ids']['subscription_id'] : '';
				//260902.0200 This poll reads only local coordinator state; PayPal is not called repeatedly while a CREATED webhook has a chance to repair an ambiguous create response.
				echo wp_json_encode(array(
					'subscription_id' => $subscription_id,
					'pending'         => !$subscription_id,
					'status'          => !empty($gateway_checkout['gateway_status']) ? (string)$gateway_checkout['gateway_status'] : '',
				));
				exit();
			}

			if($op === 'get_plan_id')
			{
				if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
				{
					echo wp_json_encode(array('error' => 'not_subscription'));
					exit();
				}

				$plan_id = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_plan_get_id($token);

				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'    => 'checkout',
						'env_setting' => $env_setting,
						'event'   => 'get_plan_id_response',
						'plan_id' => $plan_id,
						'token'   => $log_token,
					));

				if(!$plan_id)
				{
					echo wp_json_encode(array('error' => 'plan_create_failed'));
					exit();
				}

				echo wp_json_encode(array('plan_id' => $plan_id));
				exit();
			}

			if($op === 'confirm_subscription')
			{
				if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN')
				{
					echo wp_json_encode(array('error' => 'not_subscription'));
					exit();
				}
				$subscription_id = !empty($_POST['subscription_id']) ? trim(stripslashes((string)$_POST['subscription_id'])) : '';

				if(!$subscription_id)
				{
					echo wp_json_encode(array('error' => 'missing_subscription_id'));
					exit();
				}

				$gateway_checkout_id = !empty($token['gateway_checkout_id']) && c_ws_plugin__s2member_gateway_checkouts::valid_id((string)$token['gateway_checkout_id']) ? (string)$token['gateway_checkout_id'] : '';
				if($gateway_checkout_id)
				{
					$gateway_checkout = c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id);
					$expected_subscription_id = $gateway_checkout && !empty($gateway_checkout['gateway_ids']['subscription_id']) ? (string)$gateway_checkout['gateway_ids']['subscription_id'] : '';
					//260901.2145 A coordinator-backed browser may confirm only the PayPal subscription that s2Member created and persisted for this logical checkout.
					if(!$expected_subscription_id || !hash_equals($expected_subscription_id, $subscription_id))
					{
						echo wp_json_encode(array('error' => 'gateway_checkout_subscription_mismatch'));
						exit();
					}
				}

				$subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscription_id));

				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting' => $env_setting,
						'event'           => 'subscription_get_response',
						'subscription_id' => $subscription_id,
						'subscription'    => $subscription_r,
						'token'           => $log_token,
					));

				$subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0;
				$subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '';

				$subscription = array();
				if($subscription_body)
					$subscription = json_decode($subscription_body, true);

				if(!is_array($subscription))
					$subscription = array();

				if($subscription_code < 200 || $subscription_code > 299 || empty($subscription['id']))
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting' => $env_setting,
						'event'           => 'subscription_get_failed',
						'subscription_id' => $subscription_id,
						'code'            => $subscription_code,
						'body'            => $subscription_body,
					));
					echo wp_json_encode(array('error' => 'subscription_get_failed'));
					exit();
				}
				$status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : '';

				$is_single_cycle = (isset($token['rr']) && (string)$token['rr'] === '0');
				$allow_expired_single_cycle = false;

				// PayPal can complete a single-cycle subscription immediately, returning status=EXPIRED after payment.
				if($is_single_cycle && $status === 'EXPIRED')
				{
					$lpv = '';
					$lpc = '';

					if(!empty($subscription['billing_info']['last_payment']['amount']['value']))
						$lpv = (string)$subscription['billing_info']['last_payment']['amount']['value'];

					if(!empty($subscription['billing_info']['last_payment']['amount']['currency_code']))
						$lpc = strtoupper((string)$subscription['billing_info']['last_payment']['amount']['currency_code']);

					if($lpv !== '' && $lpc !== '')
						$allow_expired_single_cycle = true;
				}

				if(!$status)
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting'     => $env_setting,
						'event'           => 'subscription_status_invalid',
						'subscription_id' => $subscription_id,
						'status'          => $status,
					));

					echo wp_json_encode(array('error' => 'subscription_status_invalid'));
					exit();
				}
				$expected_plan_id = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_plan_get_id($token);
				if($expected_plan_id && !empty($subscription['plan_id']) && (string)$subscription['plan_id'] !== (string)$expected_plan_id)
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting' => $env_setting,
						'event'           => 'plan_mismatch',
						'subscription_id' => $subscription_id,
						'expected'        => $expected_plan_id,
						'actual'          => (string)$subscription['plan_id'],
					));
					echo wp_json_encode(array('error' => 'plan_mismatch'));
					exit();
				}
				$custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : '';
				if($custom_id && $custom_id !== (string)$token['invoice'])
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting' => $env_setting,
						'event'           => 'subscription_custom_id_mismatch',
						'subscription_id' => $subscription_id,
						'expected'        => (string)$token['invoice'],
						'actual'          => $custom_id,
					));
					echo wp_json_encode(array('error' => 'subscription_custom_id_mismatch'));
					exit();
				}

				//260928.1538 Framework button and webhook share a single durable fulfillment path; do not create a second simulated IPN here.
				if(strpos((string)$token['invoice'], 's2mb-') === 0)
				{
					$fulfillment = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_button_subscription_fulfill($subscription, $token, 'browser');
					if(!empty($fulfillment['pending_activation']))
					{
						echo wp_json_encode(array('pending_activation' => TRUE, 'subscription_id' => $subscription_id, 'status' => (string)$fulfillment['status']));
						exit();
					}
					if(empty($fulfillment['ok']) || empty($fulfillment['rtn_url']) || empty($fulfillment['rtn_post']))
					{
						echo wp_json_encode(array('error' => !empty($fulfillment['error']) ? (string)$fulfillment['error'] : 'subscription_fulfillment_failed'));
						exit();
					}
					echo wp_json_encode(array('rtn_url' => $fulfillment['rtn_url'], 'rtn_post' => $fulfillment['rtn_post']));
					exit();
				}

				if($gateway_checkout_id)
				{
					if(in_array($status, array('APPROVAL_PENDING', 'APPROVED'), TRUE))
					{
						//260902.0200 Coordinator-backed Pro-Forms do not treat PayPal creation/approval-pending states as paid entitlement; the browser waits briefly for ACTIVE and the activation webhook remains an off-session fallback.
						c_ws_plugin__s2member_gateway_checkouts::update($gateway_checkout_id, array('gateway_status' => $status));
						echo wp_json_encode(array('pending_activation' => TRUE, 'subscription_id' => $subscription_id, 'status' => $status));
						exit();
					}
					if($status !== 'ACTIVE' && !$allow_expired_single_cycle)
					{
						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
							'ppco'            => 'checkout',
							'env_setting'     => $env_setting,
							'event'           => 'subscription_status_invalid',
							'subscription_id' => $subscription_id,
							'status'          => $status,
						));

						echo wp_json_encode(array('error' => 'subscription_status_invalid'));
						exit();
					}
					c_ws_plugin__s2member_gateway_checkouts::update($gateway_checkout_id, array('gateway_status' => $status));
				}
				else if(!in_array($status, array('ACTIVE', 'APPROVED', 'APPROVAL_PENDING'), TRUE) && !$allow_expired_single_cycle)
				{
					//260902.0200 Preserve existing non-coordinator PayPal Checkout button behavior until those flows migrate onto Gateway Checkout and gain the same activation polling.
					//260928.1645 Existing pre-migration browser tabs still carry the legacy PayPal Checkout token without Gateway Checkout identity. Preserve their original confirmation behavior until those in-flight tokens expire.
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting'     => $env_setting,
						'event'           => 'subscription_status_invalid',
						'subscription_id' => $subscription_id,
						'status'          => $status,
					));

					echo wp_json_encode(array('error' => 'subscription_status_invalid'));
					exit();
				}

				$subscriber_email = !empty($subscription['subscriber']['email_address']) ? (string)$subscription['subscriber']['email_address'] : '';
				$first_name = !empty($subscription['subscriber']['name']['given_name']) ? (string)$subscription['subscriber']['name']['given_name'] : '';
				$last_name  = !empty($subscription['subscriber']['name']['surname']) ? (string)$subscription['subscriber']['name']['surname'] : '';

				$paypal = array(
					'txn_type'       => 'subscr_signup',
					'payment_status' => 'Completed',
					'subscr_gateway' => 'paypal',

					'txn_id'         => $subscription_id,
					'subscr_id'      => $subscription_id,
					'subscr_baid'    => $subscription_id,
					'subscr_cid'     => $subscription_id,

					'mc_gross'       => (string)$token['amount'],
					'mc_currency'    => strtoupper((string)$token['cc']),

					'period1'        => (!empty($token['tp']) && !empty($token['tt'])) ? ((string)$token['tp'].' '.strtoupper((string)$token['tt'])) : '0 D',
					'mc_amount1'     => (!empty($token['tp']) && !empty($token['tt'])) ? (string)$token['ta'] : '0.00',

					'period3'        => ((string)$token['rp'].' '.strtoupper((string)$token['rt'])),
					'mc_amount3'     => (string)$token['amount'],
					'recurring'      => ((isset($token['rr']) && (string)$token['rr'] === '1') ? '1' : '0'),

					'invoice'        => (string)$token['invoice'],
					'custom'         => (string)$token['custom'],
					'item_name'      => (string)$token['item_name'],
					'item_number'    => (string)$token['item_number'],

					'payer_email'    => $subscriber_email,
					'first_name'     => $first_name,
					'last_name'      => $last_name,

					'option_name1'      => (string)$token['on0'],
					'option_selection1' => (string)$token['os0'],
					'option_name2'      => (string)$token['on1'],
					'option_selection2' => (string)$token['os1'],
				);

				$option_ppco_subscr = 's2m_ppco_subscr_done_'.md5($subscription_id);

				//260818.0603 Mark the Subscription done only after Notify succeeds, using the same lock as webhook activation fallback.
				$notify_result = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_notify_once($paypal, $option_ppco_subscr);
				$notify_code = !empty($notify_result['code']) ? (int)$notify_result['code'] : 0;
				$notify_msg  = !empty($notify_result['message']) ? (string)$notify_result['message'] : '';
				$notify_body = !empty($notify_result['body']) ? (string)$notify_result['body'] : '';

				if(empty($notify_result['ok']))
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting'     => $env_setting,
						'event'           => 'notify_proxy_failed',
						'subscription_id' => $subscription_id,
						'code'            => $notify_code,
						'message'         => $notify_msg,
						'body'            => $notify_body,
						'error'           => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed',
					));

					echo wp_json_encode(array('error' => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed'));
					exit();
				}

				if(!empty($notify_result['duplicate']))
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting'     => $env_setting,
						'event'           => 'duplicate_subscription_ignored',
						'subscription_id' => $subscription_id,
						'option'          => $option_ppco_subscr,
					));
				else
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'            => 'checkout',
						'env_setting'     => $env_setting,
						'event'           => 'notify_proxy_response',
						'subscription_id' => $subscription_id,
						'code'            => $notify_code,
						'message'         => $notify_msg,
						'body'            => $notify_body,
					));

					//260818.0603 Only successful first-pass fulfillment should trigger replacement-subscription cancellation.
					if(!empty($notify_result['processed']) && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $subscription_id), get_defined_vars()))
						c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars);
				}

				$return_url = (string)$token['return'];
				$return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url);

				$return_post = array_merge($paypal, array(
					's2member_paypal_proxy'     => 'paypal',
					's2member_paypal_proxy_use' => 'paypal_checkout',
				));

				//260817 Sign the exact browser-return payload without exposing the reusable internal PayPal proxy key.
				$return_handoff = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_return_handoff_create($return_post);
				if(!$return_handoff)
				{
					if(!headers_sent())
						status_header(500);

					echo wp_json_encode(array('error' => 'return_handoff_failed'));
					exit();
				}
				$return_post['s2member_paypal_checkout_handoff'] = $return_handoff;

				echo wp_json_encode(array(
					'rtn_url'  => $return_url,
					'rtn_post' => $return_post,
				));
				exit();
			}

			if($op === 'cancel_subscription')
			{
				if(!is_user_logged_in())
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'  => 'checkout',
						'env_setting' => $env_setting,
						'event' => 'cancel_subscription_not_logged_in',
						'token' => $log_token,
					));

					echo wp_json_encode(array('error' => 'not_logged_in'));
					exit();
				}
				$user_id = (int)get_current_user_id();

				$nonce = !empty($_POST['s2member_paypal_checkout_nonce']) ? trim(stripslashes((string)$_POST['s2member_paypal_checkout_nonce'])) : '';
				if(!$nonce || !wp_verify_nonce($nonce, 's2m_ppco_cancel_'.$user_id))
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'   => 'checkout',
						'env_setting' => $env_setting,
						'event'  => 'cancel_subscription_bad_nonce',
						'user_id'=> $user_id,
					));

					echo wp_json_encode(array('error' => 'bad_nonce'));
					exit();
				}

				$token_user_id  = !empty($token['user_id']) ? (int)$token['user_id'] : 0;
				$token_subscr_id = !empty($token['subscr_id']) ? (string)$token['subscr_id'] : '';

				if(!$token_user_id || $token_user_id !== $user_id || !$token_subscr_id)
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'    => 'checkout',
						'env_setting' => $env_setting,
						'event'   => 'cancel_subscription_token_mismatch',
						'user_id' => $user_id,
						'token'   => $log_token,
					));

					echo wp_json_encode(array('error' => 'token_mismatch'));
					exit();
				}

				$subscr_id = (string)get_user_option('s2member_subscr_id', $user_id);
				if(!$subscr_id || $subscr_id !== $token_subscr_id)
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'       => 'checkout',
						'env_setting' => $env_setting,
						'event'      => 'cancel_subscription_user_mismatch',
						'user_id'    => $user_id,
						'user_subscr'=> $subscr_id,
						'token_subscr'=> $token_subscr_id,
					));

					echo wp_json_encode(array('error' => 'user_mismatch'));
					exit();
				}

				$reason = !empty($_POST['reason']) ? trim(stripslashes((string)$_POST['reason'])) : 'Cancelled by subscriber.';
				$reason = sanitize_text_field($reason);
				if(!$reason)
					$reason = 'Cancelled by subscriber.';

				//260819.0417 Resolve the active subscription through whichever configured PayPal API family owns it.
				$ipn_signup_vars = get_user_option('s2member_ipn_signup_vars', $user_id);
				$ipn_signup_vars = (is_array($ipn_signup_vars) && !empty($ipn_signup_vars['subscr_id']) && (string)$ipn_signup_vars['subscr_id'] === (string)$subscr_id) ? $ipn_signup_vars : array();

				$next_billing_time = '';
				$eot = c_ws_plugin__s2member_utils_users::get_user_eot($user_id, TRUE, 'next');
				if(is_array($eot) && !empty($eot['type']) && $eot['type'] === 'next' && !empty($eot['time']) && (int)$eot['time'] > time())
					$next_billing_time = gmdate('Y-m-d\TH:i:s\Z', (int)$eot['time']);

				$cancelled = c_ws_plugin__s2member_utilities::cancel_gateway_subscription(
					'paypal',
					$subscr_id,
					(string)get_user_option('s2member_subscr_baid', $user_id),
					(string)get_user_option('s2member_subscr_cid', $user_id),
					$ipn_signup_vars,
					TRUE,
					$reason
				);

				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
					'ppco'        => 'checkout',
					'env_setting' => $env_setting,
					'event'       => 'cancel_subscription_response',
					'user_id'     => $user_id,
					'subscr_id'   => $subscr_id,
					'accepted'    => $cancelled ? 1 : 0,
				));

				if($cancelled)
				{
					// Immediately feed s2Member's existing cancel handler (webhooks may be missing in MVP sites).
					$paypal = array(
						'txn_type'       => 'subscr_cancel',
						'payment_status' => 'Completed',
						'subscr_gateway' => 'paypal',

						'txn_id'    => $subscr_id,
						'subscr_id' => $subscr_id,
						'custom'    => (string)get_user_option('s2member_custom', $user_id),

						// Help legacy notify logic resolve user in some fallback cases.
						'mp_id'                => $subscr_id,
						'recurring_payment_id' => $subscr_id,

						//260517 Provide safe defaults when signup vars are missing.
						'item_number' => (string)c_ws_plugin__s2member_user_access::user_access_level(wp_get_current_user()),
						'item_name'   => 'PayPal Checkout Subscription',

						// Best-effort payer email for logs/fallback logic.
						'payer_email' => (string)wp_get_current_user()->user_email,
					);

					//260517 Enrich with stored signup vars so legacy cancel handler can match and compute EOT.
					if($ipn_signup_vars)
					{
						if(!empty($ipn_signup_vars['item_number']))
							$paypal['item_number'] = (string)$ipn_signup_vars['item_number'];

						if(!empty($ipn_signup_vars['item_name']))
							$paypal['item_name'] = (string)$ipn_signup_vars['item_name'];

						if(empty($paypal['period1']) && !empty($ipn_signup_vars['period1']))
							$paypal['period1'] = (string)$ipn_signup_vars['period1'];

						if(empty($paypal['period3']) && !empty($ipn_signup_vars['period3']))
							$paypal['period3'] = (string)$ipn_signup_vars['period3'];
					}

					$notify_url  = home_url('/?s2member_paypal_notify=1');
					$notify_post = array_merge($paypal, array(
						'proxy_user_id'                      => $user_id, //260517
						'proxy_next_billing_time'            => $next_billing_time, //260517
						's2member_paypal_proxy'              => 'paypal',
						's2member_paypal_proxy_use'          => 'paypal_checkout',
						's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(),
					));

					$notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true);

					if(!is_array($notify_r))
						$notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => '');

					$notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0;
					if(!($notify_code >= 200 && $notify_code <= 299))
					{
						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
							'ppco'        => 'checkout',
							'env_setting' => $env_setting,
							'event'       => 'cancel_subscription_notify_failed',
							'user_id'     => $user_id,
							'subscr_id'   => $subscr_id,
							'notify_code' => $notify_code,
							'notify_msg'  => !empty($notify_r['message']) ? (string)$notify_r['message'] : '',
						));
					}

					echo wp_json_encode(array('ok' => 1));
					exit();
				}

				echo wp_json_encode(array('error' => 'cancel_failed'));
				exit();
			}

			if($op === 'create_order')
			{
				$order = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_create($token);

				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'  => 'checkout',
						'env_setting' => $env_setting,
						'event' => 'create_order_response',
						'order' => $order,
						'token' => $log_token,
					));

				if(empty($order['id']))
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
							'ppco'  => 'checkout',
							'env_setting' => $env_setting,
							'event' => 'order_create_failed',
							'order' => $order,
							'token' => $log_token,
						));

					$error = !empty($order['__error']) ? (string)$order['__error'] : 'order_create_failed';
					$recoverable = ($error === 'gateway_checkout_busy');
					//260902.0646 Only an overlapping request can populate a missing order ID asynchronously; an ambiguous provider create has no pre-approval webhook, so tell the customer to retry the same idempotent checkout instead of polling pointlessly.
					echo wp_json_encode(array('error' => $error, 'recoverable' => $recoverable, 'retryable' => ($error === 'order_create_unresolved')));
					exit();
				}
				echo wp_json_encode(array('order_id' => $order['id']));
				exit();
			}
			else if($op === 'get_order_status')
			{
				//260907.1820 This recovery endpoint is intentionally coordinator-only: the signed checkout token authorizes a local state read, while PayPal polling/retries remain server/webhook responsibilities.
				$gateway_checkout_id = !empty($token['gateway_checkout_id']) && c_ws_plugin__s2member_gateway_checkouts::valid_id((string)$token['gateway_checkout_id']) ? (string)$token['gateway_checkout_id'] : '';
				//260928.1703 Read fresh option state during capture-loss polling: a webhook may have fulfilled the checkout in another PHP worker moments earlier.
				$gateway_checkout = $gateway_checkout_id ? c_ws_plugin__s2member_gateway_checkouts::load_state_uncached($gateway_checkout_id) : FALSE;
				if(!$gateway_checkout || (string)$gateway_checkout['gateway'] !== 'paypal_checkout' || (string)$gateway_checkout['operation'] !== 'payment')
				{
					echo wp_json_encode(array('error' => 'gateway_checkout_invalid'));
					exit();
				}

				$private_context = c_ws_plugin__s2member_gateway_checkouts::private_context_get($gateway_checkout_id);
				$fulfillment_result = is_array($private_context) && !empty($private_context['paypal_checkout']['fulfillment_result']) && is_array($private_context['paypal_checkout']['fulfillment_result']) ? $private_context['paypal_checkout']['fulfillment_result'] : array();
				//260902.0635 Poll only local coordinator state while independent PayPal webhooks resolve delayed creates/captures; do not hammer the provider from the browser.
				$fulfilled = ((string)$gateway_checkout['fulfillment_status'] === 'fulfilled' && !empty($fulfillment_result['rtn_url']) && !empty($fulfillment_result['rtn_post']));
				$response = array(
					'order_id'           => !empty($gateway_checkout['gateway_ids']['order_id']) ? (string)$gateway_checkout['gateway_ids']['order_id'] : '',
					'capture_id'         => !empty($gateway_checkout['gateway_ids']['capture_id']) ? (string)$gateway_checkout['gateway_ids']['capture_id'] : '',
					'status'             => !empty($gateway_checkout['gateway_status']) ? (string)$gateway_checkout['gateway_status'] : '',
					'fulfillment_status' => !empty($gateway_checkout['fulfillment_status']) ? (string)$gateway_checkout['fulfillment_status'] : '',
					'fulfilled'          => $fulfilled,
				);
				//260928.1703 A lost capture response can be recovered with the already-signed return handoff; only the original encrypted checkout token can reach this endpoint.
				if($fulfilled)
				{
					$response['rtn_url'] = $fulfillment_result['rtn_url'];
					$response['rtn_post'] = $fulfillment_result['rtn_post'];
				}
				echo wp_json_encode($response);
				exit();
			}
			else if($op === 'capture_order')
			{
				$order_id = !empty($_POST['order_id']) ? trim(stripslashes((string)$_POST['order_id'])) : '';

				if(!$order_id)
				{
					echo wp_json_encode(array('error' => 'missing_order_id'));
					exit();
				}

				$gateway_checkout_id = !empty($token['gateway_checkout_id']) && c_ws_plugin__s2member_gateway_checkouts::valid_id((string)$token['gateway_checkout_id']) ? (string)$token['gateway_checkout_id'] : '';
				if($gateway_checkout_id)
				{
					$gateway_checkout = c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id);
					$private_context = $gateway_checkout ? c_ws_plugin__s2member_gateway_checkouts::private_context_get($gateway_checkout_id) : FALSE;
					$fulfillment_result = is_array($private_context) && !empty($private_context['paypal_checkout']['fulfillment_result']) && is_array($private_context['paypal_checkout']['fulfillment_result']) ? $private_context['paypal_checkout']['fulfillment_result'] : array();
					if($gateway_checkout && (string)$gateway_checkout['fulfillment_status'] === 'fulfilled' && !empty($fulfillment_result['rtn_url']) && !empty($fulfillment_result['rtn_post']))
					{
						//260902.0646 A webhook may have finished checkout while the browser was gone; return the saved browser result locally without touching PayPal or repeating fulfillment.
						echo wp_json_encode(array('rtn_url' => $fulfillment_result['rtn_url'], 'rtn_post' => $fulfillment_result['rtn_post']));
						exit();
					}
				}

				$capture = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_capture($order_id, $token);

				$cap0 = (!empty($capture['purchase_units'][0]['payments']['captures'][0]) && is_array($capture['purchase_units'][0]['payments']['captures'][0])) ? $capture['purchase_units'][0]['payments']['captures'][0] : array();
				c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
					'ppco'       => 'checkout',
					'env_setting' => $env_setting,
					'event'      => 'capture_response',
					'order_id'   => $order_id,
					'status'     => !empty($capture['status']) ? (string)$capture['status'] : '',
					'capture_id' => !empty($cap0['id']) ? (string)$cap0['id'] : '',
					'amount'     => !empty($cap0['amount']['value']) ? (string)$cap0['amount']['value'] : '',
					'cc'         => !empty($cap0['amount']['currency_code']) ? (string)$cap0['amount']['currency_code'] : '',
					'payer'      => !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '',
					'capture'    => $capture,
					'token'      => $log_token,
				));

				if($gateway_checkout_id)
				{
					if(!empty($capture['__error']))
					{
						$error = (string)$capture['__error'];
						$recoverable = in_array($error, array('capture_pending', 'order_capture_unresolved', 'gateway_checkout_busy'), TRUE);
						echo wp_json_encode(array('error' => $error, 'recoverable' => $recoverable, 'pending' => ($error === 'capture_pending')));
						exit();
					}

					$fulfillment = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_fulfill($capture, $token);
					if(empty($fulfillment['ok']) || empty($fulfillment['rtn_url']) || empty($fulfillment['rtn_post']))
					{
						echo wp_json_encode(array('error' => !empty($fulfillment['error']) ? (string)$fulfillment['error'] : 'order_fulfillment_failed'));
						exit();
					}

					echo wp_json_encode(array('rtn_url' => $fulfillment['rtn_url'], 'rtn_post' => $fulfillment['rtn_post']));
					exit();
				}

				if(!empty($capture['__error']))
				{
					echo wp_json_encode(array('error' => (string)$capture['__error']));
					exit();
				}

				if(empty($capture['status']) || strtoupper($capture['status']) !== 'COMPLETED')
				{
					echo wp_json_encode(array('error' => 'order_capture_failed'));
					exit();
				}

				/*
				 * Build PayPal-like variables to feed s2Member's existing IPN + Return handlers.
				 */
				$payer_email = !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '';
				$first_name  = !empty($capture['payer']['name']['given_name']) ? (string)$capture['payer']['name']['given_name'] : '';
				$last_name   = !empty($capture['payer']['name']['surname']) ? (string)$capture['payer']['name']['surname'] : '';

				$pu_amount   = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['value']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['value'] : '';
				$pu_cc       = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code'] : '';
				$pu_cap_id   = !empty($capture['purchase_units'][0]['payments']['captures'][0]['id']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['id'] : '';

				if(!$payer_email || !$pu_amount || !$pu_cc || !$pu_cap_id)
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
							'ppco'     => 'checkout',
							'env_setting' => $env_setting,
							'event'    => 'capture_missing_fields',
							'order_id' => $order_id,
							'capture'  => $capture,
							'token'    => $log_token,
						));

					echo wp_json_encode(array('error' => 'capture_missing_fields'));
					exit();
				}

				// Extra safety: enforce token matches amount/currency/invoice/custom if provided.
				//260228 Normalize amount strings before comparison (e.g. 20 vs 20.00).
				if(!empty($token['amount']) && number_format((float)$token['amount'], 2, '.', '') !== number_format((float)$pu_amount, 2, '.', ''))
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'     => 'checkout',
						'env_setting' => $env_setting,
						'event'    => 'amount_mismatch',
						'order_id' => $order_id,
						'token'    => $log_token,
						'pu'       => array('amount' => $pu_amount, 'cc' => $pu_cc),
					));
					echo wp_json_encode(array('error' => 'amount_mismatch'));
					exit();
				}
				if(!empty($token['cc']) && strtoupper((string)$token['cc']) !== strtoupper((string)$pu_cc))
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'     => 'checkout',
						'env_setting' => $env_setting,
						'event'    => 'currency_mismatch',
						'order_id' => $order_id,
						'token'    => $log_token,
						'pu'       => array('amount' => $pu_amount, 'cc' => $pu_cc),
					));
					echo wp_json_encode(array('error' => 'currency_mismatch'));
					exit();
				}
				$cap_invoice_id = '';
				if(!empty($capture['purchase_units'][0]['invoice_id']))
					$cap_invoice_id = (string)$capture['purchase_units'][0]['invoice_id'];
				else if(!empty($capture['purchase_units'][0]['payments']['captures'][0]['invoice_id']))
					$cap_invoice_id = (string)$capture['purchase_units'][0]['payments']['captures'][0]['invoice_id'];

				if($cap_invoice_id && $cap_invoice_id !== (string)$token['invoice'])
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'     => 'checkout',
						'env_setting' => $env_setting,
						'event'    => 'invoice_mismatch',
						'order_id' => $order_id,
						'token'    => $log_token,
						'invoice'  => $cap_invoice_id,
					));
					echo wp_json_encode(array('error' => 'invoice_mismatch'));
					exit();
				}

				$cap_custom_id = '';
				if(!empty($capture['purchase_units'][0]['custom_id']))
					$cap_custom_id = (string)$capture['purchase_units'][0]['custom_id'];
				else if(!empty($capture['purchase_units'][0]['payments']['captures'][0]['custom_id']))
					$cap_custom_id = (string)$capture['purchase_units'][0]['payments']['captures'][0]['custom_id'];

				if($cap_custom_id && !empty($token['custom']) && $cap_custom_id !== (string)$token['custom'])
				{
					c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
						'ppco'     => 'checkout',
						'env_setting' => $env_setting,
						'event'    => 'custom_mismatch',
						'order_id' => $order_id,
						'token'    => $log_token,
						'custom'   => array(
							'token'   => !empty($token['custom']) ? $token['custom'] : '',
							'paypal'  => $cap_custom_id,
						),
					));
					echo wp_json_encode(array('error' => 'custom_mismatch'));
					exit();
				}

				$paypal = array(
					'txn_type'       => 'web_accept',
					'payment_status' => 'Completed',
					'subscr_gateway' => 'paypal',

					'txn_id'         => $pu_cap_id,
					'subscr_id'      => $pu_cap_id,
					'subscr_baid'    => $pu_cap_id,
					'subscr_cid'     => $pu_cap_id,

					'mc_gross'       => $pu_amount,
					'mc_currency'    => strtoupper($pu_cc),

					'invoice'        => (string)$token['invoice'],
					'custom'         => (string)$token['custom'],
					'item_name'      => (string)$token['item_name'],
					'item_number'    => (string)$token['item_number'],

					'payer_email'    => $payer_email,
					'first_name'     => $first_name,
					'last_name'      => $last_name,

					// Preserve s2Member's tracking option fields.
					'option_name1'      => (string)$token['on0'],
					'option_selection1' => (string)$token['os0'],
					'option_name2'      => (string)$token['on1'],
					'option_selection2' => (string)$token['os1'],
				);

				//260827.0051 Keep AJAX capture fulfillment aligned with the redirect capture path so Pro-Form tax, email/coupon routing, and resolved success URLs survive the shared Framework handler.
				if(isset($token['tax']))
					$paypal['tax'] = (string)$token['tax'];

				$is_independent_ccaps_sale = (strpos((string)$token['item_number'], '*:') === 0);
				$is_specific_post_page_sale = (strpos((string)$token['item_number'], 'sp:') === 0);
				$can_cancel_old_subscr = (!$is_independent_ccaps_sale && !$is_specific_post_page_sale); //260407 Only membership replacement-style PPCO purchases should cancel an existing recurring subscription here.

				$proxy_use = !empty($token['s2member_paypal_proxy_use']) ? (string)$token['s2member_paypal_proxy_use'] : 'paypal_checkout';
				$notify_extra = array();

				if(!empty($token['s2member_paypal_proxy_coupon']) && is_array($token['s2member_paypal_proxy_coupon']))
					$notify_extra['s2member_paypal_proxy_coupon'] = $token['s2member_paypal_proxy_coupon'];
				if(array_key_exists('s2member_paypal_proxy_return_url', $token))
					$notify_extra['s2member_paypal_proxy_return_url'] = (string)$token['s2member_paypal_proxy_return_url'];

				$notify_done_option = 's2m_ppco_capture_done_'.md5($pu_cap_id);
				$notify_result = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_notify_once($paypal, $notify_done_option, $proxy_use, $notify_extra);

				if(empty($notify_result['ok']))
					{
						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
							'ppco'        => 'checkout',
							'env_setting' => $env_setting,
							'event'       => 'notify_proxy_failed',
							'order_id'    => $order_id,
							'txn_id'      => $pu_cap_id,
							'code'        => !empty($notify_result['code']) ? (int)$notify_result['code'] : 0,
							'message'     => !empty($notify_result['message']) ? (string)$notify_result['message'] : '',
							'body'        => !empty($notify_result['body']) ? (string)$notify_result['body'] : '',
						));
						echo wp_json_encode(array('error' => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed'));
						exit();
					}

				if(!empty($notify_result['processed']))
					{
						c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array(
							'ppco'        => 'checkout',
							'env_setting' => $env_setting,
							'event'       => 'notify_proxy_response',
							'order_id'    => $order_id,
							'txn_id'      => $pu_cap_id,
							'code'        => !empty($notify_result['code']) ? (int)$notify_result['code'] : 0,
							'message'     => !empty($notify_result['message']) ? (string)$notify_result['message'] : '',
							'body'        => !empty($notify_result['body']) ? (string)$notify_result['body'] : '',
						));

						//260407 Framework PPCO AJAX replacements can also replace subscriptions created by other gateways without affecting independent CCAPS or specific post/page purchases.
						if($can_cancel_old_subscr && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $pu_cap_id), get_defined_vars())) //260406
							c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); //260407
					}

				// 2) Send the user through the existing Return handler via POST (sets cookies, thank-you UX, reg tokens, etc).
				$return_url = (string)$token['return'];
				$return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url);

				$return_post = array_merge($paypal, array(
					's2member_paypal_proxy'     => 'paypal',
					's2member_paypal_proxy_use' => $proxy_use,
				));

				//260827.0051 Carry the Pro-Form's resolved success URL inside the signed browser return; Specific Post/Page uses the Notify response body for its generated access URL.
				if(array_key_exists('s2member_paypal_proxy_return_url', $token))
					$return_post['s2member_paypal_proxy_return_url'] = !empty($notify_result['body']) ? trim((string)$notify_result['body']) : '';

				//260817 Sign the exact browser-return payload without exposing the reusable internal PayPal proxy key.
				$return_handoff = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_return_handoff_create($return_post);
				if(!$return_handoff)
				{
					if(!headers_sent())
						status_header(500);

					echo wp_json_encode(array('error' => 'return_handoff_failed'));
					exit();
				}
				$return_post['s2member_paypal_checkout_handoff'] = $return_handoff;

				echo wp_json_encode(array(
					'rtn_url'  => $return_url,
					'rtn_post' => $return_post,
				));
				exit();
			}

			echo wp_json_encode(array('error' => 'unknown_op'));
			exit();
		}
	}
}

```
