PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 261001
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v261001
261001 260927 260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 All 191 releases
← All changes | src/includes/classes/sc-paypal-button-in.inc.php +114 -36 260814 → 261001 View file →
@@ -74,8 +74,12 @@
74 74
75 75 $force_notify_url_scheme = apply_filters("ws_plugin__s2member_during_sc_paypal_button_force_notify_url_scheme", null, get_defined_vars ());
76 76 $force_return_url_scheme = apply_filters("ws_plugin__s2member_during_sc_paypal_button_force_return_url_scheme", null, get_defined_vars ());
77 77
78 + //260918.2104 TO-DO PayPal Checkout cache hardening: do not embed the one-hour, visitor-specific transaction token in rendered page HTML.
79 + // Render a cache-safe encrypted checkout definition instead, then mint the short-lived invoice/IP/user-context transaction token server-side
80 + // when checkout actually starts (output="button", "anchor", or "url"), preserving validation, idempotency, subscription context, and return/cancel behavior.
81 +
78 82 // PayPal Checkout SDK memoization (per request; shared across all button variants).
79 83 static $ppco_sdks = array();
80 84
81 85 foreach(array_keys(get_defined_vars())as$__v)$__refs[$__v]=&$$__v;
@@ -195,8 +199,12 @@
195 199
196 200 $ppco_btn_id = 's2member_ppco_cancel_'.md5($user_id.$subscr_id);
197 201 $ppco_msg_id = 's2member_ppco_cancel_msg_'.md5($user_id.$subscr_id);
198 202
203 + //260913.1946 Validate Pro's optional success URL before exposing it to cancellation JavaScript; shortcode attributes may be authored by Editors.
204 + $ppco_success_url = (c_ws_plugin__s2member_utils_conds::pro_is_installed() && !empty($attr["success"]) && is_string($attr["success"])) ? wp_validate_redirect($attr["success"], '') : '';
205 + $ppco_success_json = wp_json_encode($ppco_success_url, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
206 +
199 207 $code = '<style type="text/css">#'.esc_attr($ppco_btn_id).'{display:inline-flex;align-items:center;justify-content:center;width:150px;height:40px;padding:10px 0;border-radius:4px;border:1px solid rgba(0,0,0,0.06);background:#ffc439;color:#003087;font-family:Helvetica, Arial, sans-serif;font-size:14px;font-weight:600;cursor:pointer;box-sizing:border-box;white-space:nowrap;}#'.esc_attr($ppco_btn_id).':hover{filter:brightness(0.98);}#'.esc_attr($ppco_btn_id).':disabled{opacity:0.65;cursor:not-allowed;}</style>'."\n";
200 208 $code .= '<button type="button" id="'.esc_attr($ppco_btn_id).'">'.esc_html(_x('Unsubscribe', 'paypal cancellation button label', 's2member')).'</button>'."\n"; //260218
201 209 $code .= '<div id="'.esc_attr($ppco_msg_id).'" style="display:none; margin-top:8px;"></div>'."\n";
202 210 $code .= '<script type="text/javascript">'."\n";
@@ -203,8 +211,9 @@
203 211 $code .= '(function(){'."\n";
204 212 $code .= 'var b=document.getElementById("'.esc_js($ppco_btn_id).'");'."\n";
205 213 $code .= 'var m=document.getElementById("'.esc_js($ppco_msg_id).'");'."\n";
206 214 $code .= 'var u="'.esc_js($pp_manage_url).'";'."\n";
215 + $code .= 'var s='.$ppco_success_json.';'."\n";
207 216 $code .= 'function goManage(){try{var w=window.open(u,"_blank","noopener");if(!w){window.location.href=u;}}catch(e){window.location.href=u;}}'."\n";
208 217 $code .= 'function show(msg){try{if(m){m.style.display="block";m.innerHTML=msg;}}catch(e){}}'."\n";
209 218 $code .= 'function enc(o){var s=[];for(var k in o){if(!o.hasOwnProperty(k))continue;s.push(encodeURIComponent(k)+"="+encodeURIComponent(o[k]));}return s.join("&");}'."\n";
210 219 $code .= 'if(!b){return;}'."\n";
@@ -214,9 +223,9 @@
214 223 $code .= 'if(!window.confirm("'.esc_js(__('Cancel your subscription now?', 's2member')).'")){return;}'."\n"; //260218
215 224 $code .= 'b.disabled=true;'."\n";
216 225 $code .= 'fetch("'.esc_js($ppco_endpoint).'",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"cancel_subscription",s2member_paypal_checkout_t:"'.esc_js($ppco_token).'",s2member_paypal_checkout_nonce:"'.esc_js($ppco_nonce).'"} )})'."\n";
217 226 $code .= '.then(function(r){return r.json();})'."\n";
218 - $code .= '.then(function(res){if(res&&res.ok){show("'.esc_js(__('Subscription cancelled.', 's2member')).'");}else{goManage(); b.disabled=false;}})'."\n"; //260218
227 + $code .= '.then(function(res){if(res&&res.ok){if(s){window.location.assign(s);}else{show("'.esc_js(__('Subscription cancelled.', 's2member')).'");}}else{goManage(); b.disabled=false;}})'."\n"; //260913.1946 Honor Pro's validated success URL after a confirmed on-site cancellation.
219 228 $code .= '.catch(function(){goManage(); b.disabled=false;});'."\n";
220 229 $code .= '});'."\n";
221 230 $code .= '})();'."\n";
222 231 $code .= '</script>'."\n";
@@ -287,8 +296,16 @@
287 296 $paypal_os1_input_value = /* Current User's IP Address for tracking purposes. */ c_ws_plugin__s2member_utils_ip::current();
288 297
289 298 $paypal_invoice_input_value = /* s2Member's Unique Code~IP combo. */ uniqid () . "~" . c_ws_plugin__s2member_utils_ip::current();
290 299
300 + //260928.1515 Give each rendered PayPal Checkout button a signed provisional identity; persist its Gateway Checkout only when the buyer starts, never on page render.
301 + $ppco_gateway_checkout_identity = FALSE;
302 + if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
303 + {
304 + $ppco_gateway_checkout_identity = c_ws_plugin__s2member_gateway_checkouts::browser_identity();
305 + $paypal_invoice_input_value = 's2mb-'.(string)$ppco_gateway_checkout_identity['id'];
306 + }
307 +
291 308 $attr["sp_ids_exp"] = /* Combined "sp:ids:expiration hours". */ "sp:" . $attr["ids"] . ":" . $attr["exp"];
292 309
293 310 $success_return_url = /* s2Member handles this all by itself. However, it can be Filtered. */ home_url ("/?s2member_paypal_return=1", $force_return_url_scheme);
294 311 $success_return_url = apply_filters("ws_plugin__s2member_during_sc_paypal_button_success_return_url", $success_return_url, get_defined_vars ());
@@ -346,8 +363,15 @@
346 363
347 364 'checksum' => md5($paypal_invoice_input_value.c_ws_plugin__s2member_utils_ip::current().$attr["sp_ids_exp"]),
348 365 );
349 366
367 + //260928.1515 Bind the PayPal invoice and encrypted purchase terms to the same provisional Gateway Checkout identity, including in anchor/url mode.
368 + if($ppco_gateway_checkout_identity)
369 + {
370 + $ppco_token['gateway_checkout_id'] = (string)$ppco_gateway_checkout_identity['id'];
371 + $ppco_token['gateway_checkout_token'] = (string)$ppco_gateway_checkout_identity['token'];
372 + }
373 +
350 374 $ppco_token = urlencode(c_ws_plugin__s2member_utils_encryption::encrypt(serialize($ppco_token)));
351 375
352 376 // output="anchor|url" support (no JS SDK; redirects through s2Member, then to PayPal approval URL).
353 377 if($attr["output"] === "anchor" || $attr["output"] === "url")
@@ -396,9 +420,9 @@
396 420 else
397 421 $ppco_sdk_src = $ppco_sdk_src.'?client-id='.rawurlencode($ppco_client_id).'&currency='.rawurlencode($ppco_cc).'&intent=capture&commit=true&disable-funding=card'.$ppco_buyer_country_q.$ppco_locale_q;
398 422
399 423 $code = '<div id="'.esc_attr($ppco_div_id).'" class="ws-plugin--s2member-paypal-button ws-plugin--s2member-ppco-button" style="max-width:145px; width:auto; margin:0;"></div>'."\n";
400 - $code .= '<div id="'.esc_attr($ppco_err_id).'" class="ws-plugin--s2member-ppco-error" style="display:none; margin:0;"></div>'."\n";
424 + $code .= '<div id="'.esc_attr($ppco_err_id).'" class="ws-plugin--s2member-ppco-message" style="display:none; margin:0;"></div>'."\n";
401 425
402 426 $ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
403 427
404 428 if($ppco_sdk_just_loaded)
@@ -411,30 +435,38 @@
411 435 $code .= 'var d="'.esc_js($ppco_div_id).'";'."\n";
412 436 $code .= 'var e="'.esc_js($ppco_err_id).'";'."\n";
413 437 $code .= 'var t="'.esc_js($ppco_token).'";'."\n";
414 438 $code .= 'var u="'.esc_js($ppco_endpoint).'";'."\n";
439 + //260928.1739 Bind to the SDK script actually emitted above and encode its URL as JavaScript, not HTML entities.
415 440 $code .= 'var ns="'.esc_js($ppco_sdk_ns).'";'."\n";
416 - $code .= 'var s="'.esc_js($ppco_sdk_src).'";'."\n";
441 + $code .= 'var sdkId="'.esc_js($ppco_sdk_id).'";'."\n";
442 + $code .= 'var s='.wp_json_encode($ppco_sdk_src, JSON_HEX_AMP | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT).';'."\n";
417 443 $code .= 'var cid="'.esc_js($paypal_invoice_input_value).'";'."\n";
418 - $code .= 'function showErr(m){try{var el=document.getElementById(e);if(el){el.style.display="block";el.innerHTML=m;}}catch(x){}}'."\n";
419 - $code .= 'function postTo(url, data){var f=document.createElement("form");f.method="post";f.action=url;for(var k in data){if(!data.hasOwnProperty(k))continue;var i=document.createElement("input");i.type="hidden";i.name=k;i.value=data[k];f.appendChild(i);}document.body.appendChild(f);f.submit();}'."\n";
444 + //260819.0042 Keep standalone Checkout feedback consistent with Pro-Forms while distinguishing cancellation from errors.
445 + $code .= 'function showMsg(m,t){try{var el=document.getElementById(e);if(el){el.style.display="block";el.innerHTML="<span class=\"ws-plugin--s2member-ppco-"+(t==="info"?"info":"error")+"\">"+m+"</span>";}}catch(x){}}function showErr(m){showMsg(m,"error");}function showInfo(m){showMsg(m,"info");}'."\n";
446 + $code .= 'function postTo(url, data){var f=document.createElement("form");f.method="post";f.acceptCharset="UTF-8";f.action=url;for(var k in data){if(!data.hasOwnProperty(k))continue;var i=document.createElement("input");i.type="hidden";i.name=k;i.value=data[k];f.appendChild(i);}document.body.appendChild(f);f.submit();}'."\n"; //260817 Keep signed PayPal Checkout returns in UTF-8.
420 447 $code .= 'function enc(o){var a=[];for(var k in o){if(!o.hasOwnProperty(k))continue;a.push(encodeURIComponent(k)+"="+encodeURIComponent(o[k]));}return a.join("&");}'."\n";
421 - $code .= 'function loadSdk(cb){var P=window[ns];if(P&&P.Buttons){cb(P);return;}var id=ns+"_sdk",tag=document.getElementById(id),done=false;function finish(){if(done)return;done=true;cb(window[ns]&&window[ns].Buttons?window[ns]:null);}function ok(){finish();}function fail(){finish();}if(tag){if(window[ns]&&window[ns].Buttons){finish();return;}if(tag.getAttribute("src")!==s){tag.setAttribute("src",s);}if(tag.readyState==="complete"||tag.readyState==="loaded"){setTimeout(finish,0);return;}tag.addEventListener("load",ok);tag.addEventListener("error",fail);setTimeout(finish,3500);return;}tag=document.createElement("script");tag.id=id;tag.setAttribute("data-namespace",ns);tag.src=s;tag.async=true;tag.onload=ok;tag.onerror=fail;(document.head||document.body||document.documentElement).appendChild(tag);setTimeout(finish,3500);}'."\n";
448 + $code .= 'function loadSdk(cb){var P=window[ns];if(P&&P.Buttons){cb(P);return;}var id=sdkId,tag=document.getElementById(id),done=false;function finish(){if(done)return;done=true;cb(window[ns]&&window[ns].Buttons?window[ns]:null);}function ok(){finish();}function fail(){finish();}if(tag){if(window[ns]&&window[ns].Buttons){finish();return;}if(tag.getAttribute("src")!==s){tag.setAttribute("src",s);}if(tag.readyState==="complete"||tag.readyState==="loaded"){setTimeout(finish,0);return;}tag.addEventListener("load",ok);tag.addEventListener("error",fail);setTimeout(finish,3500);return;}tag=document.createElement("script");tag.id=id;tag.setAttribute("data-namespace",ns);tag.src=s;tag.async=true;tag.onload=ok;tag.onerror=fail;(document.head||document.body||document.documentElement).appendChild(tag);setTimeout(finish,3500);}'."\n";
422 449 if($ppco_intent === 'subscription')
423 450 {
424 - $code .= 'function getPlanId(){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"get_plan_id",s2member_paypal_checkout_t:t})}).then(function(r){return r.json();}).then(function(res){if(res&&res.plan_id)return res.plan_id;throw(res&&res.error?res.error:"plan_get_failed");});}'."\n";
451 + //260928.1739 WordPress rendered this inline JS with &#038;&#038; inside throw(res&&res.error), breaking the entire PayPal Button script. Use a ternary and || instead.
452 + $code .= 'function getPlanId(){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"get_plan_id",s2member_paypal_checkout_t:t})}).then(function(r){return r.json();}).then(function(res){if(res&&res.plan_id)return res.plan_id;throw(res ? (res.error || "plan_get_failed") : "plan_get_failed");});}'."\n";
425 453 $code .= 'var planId=null;'."\n";
426 454 $code .= 'function createSubscription(data,actions){if(planId)return actions.subscription.create({plan_id:planId,custom_id:cid,application_context:{shipping_preference:"NO_SHIPPING"}});return getPlanId().then(function(pid){planId=pid;return actions.subscription.create({plan_id:planId,custom_id:cid,application_context:{shipping_preference:"NO_SHIPPING"}});});}'."\n";
427 - $code .= 'function onApprove(data){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"confirm_subscription",s2member_paypal_checkout_t:t,subscription_id:(data&&data.subscriptionID?data.subscriptionID:"")})}).then(function(r){return r.json();}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}throw(res&&res.error?res.error:"subscription_confirm_failed");}).catch(function(e){showErr("Subscription could not be completed. Please try again.");});}'."\n";
428 - $code .= 'function onCancel(){showErr("Subscription cancelled.");}'."\n";
455 + //260928.1739 WordPress rendered this inline JS with &#038;&#038; inside throw(res&&res.error), breaking the entire PayPal Button script. Use a ternary and || instead.
456 + $code .= 'function onApprove(data){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"confirm_subscription",s2member_paypal_checkout_t:t,subscription_id:(data&&data.subscriptionID?data.subscriptionID:"")})}).then(function(r){return r.json();}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}throw(res ? (res.error || "subscription_confirm_failed") : "subscription_confirm_failed");}).catch(function(e){showErr("Subscription could not be completed. Please try again.");});}'."\n";
457 + $code .= 'function onCancel(){showInfo("Subscription cancelled.");}'."\n";
429 458 $code .= 'function onError(err){var m="PayPal error. Please try again.";try{if(err){if(typeof err==="string")m="PayPal error: "+err;else if(err.message)m="PayPal error: "+err.message;}}catch(x){}showErr(m);}'."\n";
430 459 $code .= 'function init(){loadSdk(function(P){var el=document.getElementById(d);if(!el){return;}if(el.getAttribute("data-s2m-ppco-rendered")==="1"){return;}if(!P||!P.Buttons){showErr("PayPal SDK failed to load.");return;}el.setAttribute("data-s2m-ppco-rendered","1");try{P.Buttons({fundingSource:P.FUNDING.PAYPAL,style:{layout:"vertical",tagline:false,height:40},createSubscription:createSubscription,onApprove:onApprove,onCancel:onCancel,onError:onError}).render("#"+d);}catch(x){el.removeAttribute("data-s2m-ppco-rendered");showErr("PayPal render failed.");}});}'."\n";
431 460 }
432 461 else
433 462 {
434 - $code .= 'function createOrder(){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"create_order",s2member_paypal_checkout_t:t})}).then(function(r){return r.json();}).then(function(res){if(res&&res.order_id)return res.order_id;throw(res&&res.error?res.error:"order_create_failed");});}'."\n";
435 - $code .= 'function onApprove(data){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"capture_order",s2member_paypal_checkout_t:t,order_id:(data&&data.orderID?data.orderID:"")})}).then(function(r){return r.json();}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}throw(res&&res.error?res.error:"order_capture_failed");}).catch(function(e){showErr("Payment could not be completed. Please try again.");});}'."\n";
436 - $code .= 'function onCancel(){showErr("Payment cancelled.");}'."\n";
463 + $code .= 'function request(op,vars){var body={s2member_paypal_checkout_op:op,s2member_paypal_checkout_t:t};for(var k in (vars||{})){if(vars.hasOwnProperty(k))body[k]=vars[k];}return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc(body)}).then(function(r){return r.json();});}'."\n";
464 + $code .= 'function createOrder(){return request("create_order").then(function(res){if(res&&res.order_id)return res.order_id;throw(res ? (res.error || "order_create_failed") : "order_create_failed");});}'."\n";
465 + //260928.1703 A completed PayPal capture can reach the verified webhook even when the browser loses its response. Poll only our durable state and submit its saved signed handoff; never issue a second capture on uncertainty.
466 + $code .= 'function recoverOrder(n){return request("get_order_status").then(function(res){if(res&&res.fulfilled&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}if(n>=20)throw "order_confirmation_pending";return new Promise(function(resolve){setTimeout(resolve,1200);}).then(function(){return recoverOrder(n+1);});});}'."\n";
467 + $code .= 'function onApprove(data){var oid=data&&data.orderID?data.orderID:"";return request("capture_order",{order_id:oid}).catch(function(){return {recoverable:true};}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}if(res&&res.recoverable)return recoverOrder(0).catch(function(){showInfo("Payment confirmation is pending. Please do not pay again; check your email or contact support.");});showErr("Payment could not be confirmed. Please contact support before trying again.");});}'."\n";
468 + $code .= 'function onCancel(){showInfo("Payment cancelled.");}'."\n";
437 469 $code .= 'function onError(err){var m="PayPal error. Please try again.";try{if(err){if(typeof err==="string")m="PayPal error: "+err;else if(err.message)m="PayPal error: "+err.message;}}catch(x){}showErr(m);}'."\n";
438 470 $code .= 'function init(){loadSdk(function(P){var el=document.getElementById(d);if(!el){return;}if(el.getAttribute("data-s2m-ppco-rendered")==="1"){return;}if(!P||!P.Buttons){showErr("PayPal SDK failed to load.");return;}el.setAttribute("data-s2m-ppco-rendered","1");try{P.Buttons({fundingSource:P.FUNDING.PAYPAL,style:{layout:"vertical",tagline:false,height:40},createOrder:createOrder,onApprove:onApprove,onCancel:onCancel,onError:onError}).render("#"+d);}catch(x){el.removeAttribute("data-s2m-ppco-rendered");showErr("PayPal render failed.");}});}'."\n";
439 471 }
440 472 $code .= 'if(document.readyState==="complete"){init();}else{window.addEventListener("load",init);}'."\n";
@@ -499,8 +531,16 @@
499 531 $paypal_os1_input_value = /* Current User's IP Address for tracking purposes. */ c_ws_plugin__s2member_utils_ip::current();
500 532
501 533 $paypal_invoice_input_value = /* s2Member's Unique Code~IP combo. */ uniqid () . "~" . c_ws_plugin__s2member_utils_ip::current();
502 534
535 + //260928.1515 Give each rendered PayPal Checkout button a signed provisional identity; persist its Gateway Checkout only when the buyer starts, never on page render.
536 + $ppco_gateway_checkout_identity = FALSE;
537 + if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
538 + {
539 + $ppco_gateway_checkout_identity = c_ws_plugin__s2member_gateway_checkouts::browser_identity();
540 + $paypal_invoice_input_value = 's2mb-'.(string)$ppco_gateway_checkout_identity['id'];
541 + }
542 +
503 543 $attr["level_ccaps_eotper"] = ($attr["rr"] === "BN" && $attr["rt"] !== "L") ? $attr["level"] . ":" . $attr["ccaps"] . ":" . $attr["rp"] . " " . $attr["rt"] : $attr["level"] . ":" . $attr["ccaps"];
504 544 $attr["level_ccaps_eotper"] = /* Clean any trailing separators from this string. */ rtrim ($attr["level_ccaps_eotper"], ":");
505 545
506 546 $success_return_url = /* s2Member handles this all by itself. However, it can be Filtered. */ home_url ("/?s2member_paypal_return=1", $force_return_url_scheme);
@@ -560,8 +600,15 @@
560 600
561 601 'checksum' => md5($paypal_invoice_input_value.c_ws_plugin__s2member_utils_ip::current().$attr["level_ccaps_eotper"]),
562 602 );
563 603
604 + //260928.1515 Bind the PayPal invoice and encrypted purchase terms to the same provisional Gateway Checkout identity, including in anchor/url mode.
605 + if($ppco_gateway_checkout_identity)
606 + {
607 + $ppco_token['gateway_checkout_id'] = (string)$ppco_gateway_checkout_identity['id'];
608 + $ppco_token['gateway_checkout_token'] = (string)$ppco_gateway_checkout_identity['token'];
609 + }
610 +
564 611 $ppco_token = urlencode(c_ws_plugin__s2member_utils_encryption::encrypt(serialize($ppco_token)));
565 612
566 613 // output="anchor|url" support (no JS SDK; redirects through s2Member, then to PayPal approval URL).
567 614 if($attr["output"] === "anchor" || $attr["output"] === "url")
@@ -610,9 +657,9 @@
610 657 else
611 658 $ppco_sdk_src = $ppco_sdk_src.'?client-id='.rawurlencode($ppco_client_id).'&currency='.rawurlencode($ppco_cc).'&intent=capture&commit=true&disable-funding=card'.$ppco_buyer_country_q.$ppco_locale_q;
612 659
613 660 $code = '<div id="'.esc_attr($ppco_div_id).'" class="ws-plugin--s2member-paypal-button ws-plugin--s2member-ppco-button" style="max-width:145px; width:auto; margin:0;"></div>'."\n";
614 - $code .= '<div id="'.esc_attr($ppco_err_id).'" class="ws-plugin--s2member-ppco-error" style="display:none; margin:0;"></div>'."\n";
661 + $code .= '<div id="'.esc_attr($ppco_err_id).'" class="ws-plugin--s2member-ppco-message" style="display:none; margin:0;"></div>'."\n";
615 662
616 663 $ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
617 664
618 665 if($ppco_sdk_just_loaded)
@@ -625,30 +672,37 @@
625 672 $code .= 'var d="'.esc_js($ppco_div_id).'";'."\n";
626 673 $code .= 'var e="'.esc_js($ppco_err_id).'";'."\n";
627 674 $code .= 'var t="'.esc_js($ppco_token).'";'."\n";
628 675 $code .= 'var u="'.esc_js($ppco_endpoint).'";'."\n";
676 + //260928.1739 Bind to the SDK script actually emitted above and encode its URL as JavaScript, not HTML entities.
629 677 $code .= 'var ns="'.esc_js($ppco_sdk_ns).'";'."\n";
630 - $code .= 'var s="'.esc_js($ppco_sdk_src).'";'."\n";
678 + $code .= 'var sdkId="'.esc_js($ppco_sdk_id).'";'."\n";
679 + $code .= 'var s='.wp_json_encode($ppco_sdk_src, JSON_HEX_AMP | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT).';'."\n";
631 680 $code .= 'var cid="'.esc_js($paypal_invoice_input_value).'";'."\n";
632 - $code .= 'function showErr(m){try{var el=document.getElementById(e);if(el){el.style.display="block";el.innerHTML=m;}}catch(x){}}'."\n";
633 - $code .= 'function postTo(url, data){var f=document.createElement("form");f.method="post";f.action=url;for(var k in data){if(!data.hasOwnProperty(k))continue;var i=document.createElement("input");i.type="hidden";i.name=k;i.value=data[k];f.appendChild(i);}document.body.appendChild(f);f.submit();}'."\n";
681 + //260819.0042 Keep standalone Checkout feedback consistent with Pro-Forms while distinguishing cancellation from errors.
682 + $code .= 'function showMsg(m,t){try{var el=document.getElementById(e);if(el){el.style.display="block";el.innerHTML="<span class=\"ws-plugin--s2member-ppco-"+(t==="info"?"info":"error")+"\">"+m+"</span>";}}catch(x){}}function showErr(m){showMsg(m,"error");}function showInfo(m){showMsg(m,"info");}'."\n";
683 + $code .= 'function postTo(url, data){var f=document.createElement("form");f.method="post";f.acceptCharset="UTF-8";f.action=url;for(var k in data){if(!data.hasOwnProperty(k))continue;var i=document.createElement("input");i.type="hidden";i.name=k;i.value=data[k];f.appendChild(i);}document.body.appendChild(f);f.submit();}'."\n"; //260817 Keep signed PayPal Checkout returns in UTF-8.
634 684 $code .= 'function enc(o){var a=[];for(var k in o){if(!o.hasOwnProperty(k))continue;a.push(encodeURIComponent(k)+"="+encodeURIComponent(o[k]));}return a.join("&");}'."\n";
635 - $code .= 'function loadSdk(cb){var P=window[ns];if(P&&P.Buttons){cb(P);return;}var id=ns+"_sdk",tag=document.getElementById(id),done=false;function finish(){if(done)return;done=true;cb(window[ns]&&window[ns].Buttons?window[ns]:null);}function ok(){finish();}function fail(){finish();}if(tag){if(window[ns]&&window[ns].Buttons){finish();return;}if(tag.getAttribute("src")!==s){tag.setAttribute("src",s);}if(tag.readyState==="complete"||tag.readyState==="loaded"){setTimeout(finish,0);return;}tag.addEventListener("load",ok);tag.addEventListener("error",fail);setTimeout(finish,3500);return;}tag=document.createElement("script");tag.id=id;tag.setAttribute("data-namespace",ns);tag.src=s;tag.async=true;tag.onload=ok;tag.onerror=fail;(document.head||document.body||document.documentElement).appendChild(tag);setTimeout(finish,3500);}'."\n";
685 + $code .= 'function loadSdk(cb){var P=window[ns];if(P&&P.Buttons){cb(P);return;}var id=sdkId,tag=document.getElementById(id),done=false;function finish(){if(done)return;done=true;cb(window[ns]&&window[ns].Buttons?window[ns]:null);}function ok(){finish();}function fail(){finish();}if(tag){if(window[ns]&&window[ns].Buttons){finish();return;}if(tag.getAttribute("src")!==s){tag.setAttribute("src",s);}if(tag.readyState==="complete"||tag.readyState==="loaded"){setTimeout(finish,0);return;}tag.addEventListener("load",ok);tag.addEventListener("error",fail);setTimeout(finish,3500);return;}tag=document.createElement("script");tag.id=id;tag.setAttribute("data-namespace",ns);tag.src=s;tag.async=true;tag.onload=ok;tag.onerror=fail;(document.head||document.body||document.documentElement).appendChild(tag);setTimeout(finish,3500);}'."\n";
636 686 if($ppco_intent === 'subscription')
637 687 {
638 - $code .= 'function getPlanId(){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"get_plan_id",s2member_paypal_checkout_t:t})}).then(function(r){return r.json();}).then(function(res){if(res&&res.plan_id)return res.plan_id;throw(res&&res.error?res.error:"plan_get_failed");});}'."\n";
688 + $code .= 'function getPlanId(){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"get_plan_id",s2member_paypal_checkout_t:t})}).then(function(r){return r.json();}).then(function(res){if(res&&res.plan_id)return res.plan_id;throw(res ? (res.error || "plan_get_failed") : "plan_get_failed");});}'."\n";
639 689 $code .= 'var planId=null;'."\n";
640 690 $code .= 'function createSubscription(data,actions){if(planId)return actions.subscription.create({plan_id:planId,custom_id:cid,application_context:{shipping_preference:"NO_SHIPPING"}});return getPlanId().then(function(pid){planId=pid;return actions.subscription.create({plan_id:planId,custom_id:cid,application_context:{shipping_preference:"NO_SHIPPING"}});});}'."\n";
641 - $code .= 'function onApprove(data){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"confirm_subscription",s2member_paypal_checkout_t:t,subscription_id:(data&&data.subscriptionID?data.subscriptionID:"")})}).then(function(r){return r.json();}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}throw(res&&res.error?res.error:"subscription_confirm_failed");}).catch(function(e){showErr("Subscription could not be completed. Please try again.");});}'."\n";
642 - $code .= 'function onCancel(){showErr("Subscription cancelled.");}'."\n";
691 + //260928.1739 WordPress rendered this inline JS with &#038;&#038; inside throw(res&&res.error), breaking the entire PayPal Button script. Use a ternary and || instead.
692 + $code .= 'function onApprove(data){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"confirm_subscription",s2member_paypal_checkout_t:t,subscription_id:(data&&data.subscriptionID?data.subscriptionID:"")})}).then(function(r){return r.json();}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}throw(res ? (res.error || "subscription_confirm_failed") : "subscription_confirm_failed");}).catch(function(e){showErr("Subscription could not be completed. Please try again.");});}'."\n";
693 + $code .= 'function onCancel(){showInfo("Subscription cancelled.");}'."\n";
643 694 $code .= 'function onError(err){var m="PayPal error. Please try again.";try{if(err){if(typeof err==="string")m="PayPal error: "+err;else if(err.message)m="PayPal error: "+err.message;}}catch(x){}showErr(m);}'."\n";
644 695 $code .= 'function init(){loadSdk(function(P){var el=document.getElementById(d);if(!el){return;}if(el.getAttribute("data-s2m-ppco-rendered")==="1"){return;}if(!P||!P.Buttons){showErr("PayPal SDK failed to load.");return;}el.setAttribute("data-s2m-ppco-rendered","1");try{P.Buttons({fundingSource:P.FUNDING.PAYPAL,style:{layout:"vertical",tagline:false,height:40},createSubscription:createSubscription,onApprove:onApprove,onCancel:onCancel,onError:onError}).render("#"+d);}catch(x){el.removeAttribute("data-s2m-ppco-rendered");showErr("PayPal render failed.");}});}'."\n";
645 696 }
646 697 else
647 698 {
648 - $code .= 'function createOrder(){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"create_order",s2member_paypal_checkout_t:t})}).then(function(r){return r.json();}).then(function(res){if(res&&res.order_id)return res.order_id;throw(res&&res.error?res.error:"order_create_failed");});}'."\n";
649 - $code .= 'function onApprove(data){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"capture_order",s2member_paypal_checkout_t:t,order_id:(data&&data.orderID?data.orderID:"")})}).then(function(r){return r.json();}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}throw(res&&res.error?res.error:"order_capture_failed");}).catch(function(e){showErr("Payment could not be completed. Please try again.");});}'."\n";
650 - $code .= 'function onCancel(){showErr("Payment cancelled.");}'."\n";
699 + $code .= 'function request(op,vars){var body={s2member_paypal_checkout_op:op,s2member_paypal_checkout_t:t};for(var k in (vars||{})){if(vars.hasOwnProperty(k))body[k]=vars[k];}return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc(body)}).then(function(r){return r.json();});}'."\n";
700 + $code .= 'function createOrder(){return request("create_order").then(function(res){if(res&&res.order_id)return res.order_id;throw(res ? (res.error || "order_create_failed") : "order_create_failed");});}'."\n";
701 + //260928.1703 A completed PayPal capture can reach the verified webhook even when the browser loses its response. Poll only our durable state and submit its saved signed handoff; never issue a second capture on uncertainty.
702 + $code .= 'function recoverOrder(n){return request("get_order_status").then(function(res){if(res&&res.fulfilled&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}if(n>=20)throw "order_confirmation_pending";return new Promise(function(resolve){setTimeout(resolve,1200);}).then(function(){return recoverOrder(n+1);});});}'."\n";
703 + $code .= 'function onApprove(data){var oid=data&&data.orderID?data.orderID:"";return request("capture_order",{order_id:oid}).catch(function(){return {recoverable:true};}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}if(res&&res.recoverable)return recoverOrder(0).catch(function(){showInfo("Payment confirmation is pending. Please do not pay again; check your email or contact support.");});showErr("Payment could not be confirmed. Please contact support before trying again.");});}'."\n";
704 + $code .= 'function onCancel(){showInfo("Payment cancelled.");}'."\n";
651 705 $code .= 'function onError(err){var m="PayPal error. Please try again.";try{if(err){if(typeof err==="string")m="PayPal error: "+err;else if(err.message)m="PayPal error: "+err.message;}}catch(x){}showErr(m);}'."\n";
652 706 $code .= 'function init(){loadSdk(function(P){var el=document.getElementById(d);if(!el){return;}if(el.getAttribute("data-s2m-ppco-rendered")==="1"){return;}if(!P||!P.Buttons){showErr("PayPal SDK failed to load.");return;}el.setAttribute("data-s2m-ppco-rendered","1");try{P.Buttons({fundingSource:P.FUNDING.PAYPAL,style:{layout:"vertical",tagline:false,height:40},createOrder:createOrder,onApprove:onApprove,onCancel:onCancel,onError:onError}).render("#"+d);}catch(x){el.removeAttribute("data-s2m-ppco-rendered");showErr("PayPal render failed.");}});}'."\n";
653 707 }
654 708 $code .= 'if(document.readyState==="complete"){init();}else{window.addEventListener("load",init);}'."\n";
@@ -713,8 +767,16 @@
713 767 $paypal_os1_input_value = /* Current User's IP Address for tracking purposes. */ c_ws_plugin__s2member_utils_ip::current();
714 768
715 769 $paypal_invoice_input_value = /* s2Member's Unique Code~IP combo. */ uniqid () . "~" . c_ws_plugin__s2member_utils_ip::current();
716 770
771 + //260928.1515 Give each rendered PayPal Checkout button a signed provisional identity; persist its Gateway Checkout only when the buyer starts, never on page render.
772 + $ppco_gateway_checkout_identity = FALSE;
773 + if(c_ws_plugin__s2member_paypal_utilities::paypal_checkout_is_enabled())
774 + {
775 + $ppco_gateway_checkout_identity = c_ws_plugin__s2member_gateway_checkouts::browser_identity();
776 + $paypal_invoice_input_value = 's2mb-'.(string)$ppco_gateway_checkout_identity['id'];
777 + }
778 +
717 779 $attr["desc"] = (!$attr["desc"]) ? $GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["level" . $attr["level"] . "_label"] : $attr["desc"];
718 780
719 781 // PayPal Checkout: rr=0 with no trial/initial should behave like Buy Now (one-time),
720 782 // so s2Member’s standard Buy Now/EOT routines run (mirrors other gateways).
@@ -803,8 +865,15 @@
803 865
804 866 'checksum' => md5($paypal_invoice_input_value.c_ws_plugin__s2member_utils_ip::current().$attr["level_ccaps_eotper"]),
805 867 );
806 868
869 + //260928.1515 Bind the PayPal invoice and encrypted purchase terms to the same provisional Gateway Checkout identity, including in anchor/url mode.
870 + if($ppco_gateway_checkout_identity)
871 + {
872 + $ppco_token['gateway_checkout_id'] = (string)$ppco_gateway_checkout_identity['id'];
873 + $ppco_token['gateway_checkout_token'] = (string)$ppco_gateway_checkout_identity['token'];
874 + }
875 +
807 876 $ppco_token = urlencode(c_ws_plugin__s2member_utils_encryption::encrypt(serialize($ppco_token)));
808 877
809 878 // output="anchor|url" support (no JS SDK; redirects through s2Member, then to PayPal approval URL).
810 879 if($attr["output"] === "anchor" || $attr["output"] === "url")
@@ -852,9 +921,9 @@
852 921 else
853 922 $ppco_sdk_src = $ppco_sdk_src.'?client-id='.rawurlencode($ppco_client_id).'&currency='.rawurlencode($ppco_cc).'&intent=capture&commit=true&disable-funding=card'.$ppco_buyer_country_q.$ppco_locale_q;
854 923
855 924 $code = '<div id="'.esc_attr($ppco_div_id).'" class="ws-plugin--s2member-paypal-button ws-plugin--s2member-ppco-button" style="max-width:145px; width:auto; margin:0;"></div>'."\n";
856 - $code .= '<div id="'.esc_attr($ppco_err_id).'" class="ws-plugin--s2member-ppco-error" style="display:none; margin:0;"></div>'."\n";
925 + $code .= '<div id="'.esc_attr($ppco_err_id).'" class="ws-plugin--s2member-ppco-message" style="display:none; margin:0;"></div>'."\n";
857 926
858 927 $ppco_sdk_src = apply_filters('ws_plugin__s2member_ppco_sdk_src', $ppco_sdk_src, get_defined_vars());
859 928
860 929 if($ppco_sdk_just_loaded)
@@ -867,30 +936,39 @@
867 936 $code .= 'var d="'.esc_js($ppco_div_id).'";'."\n";
868 937 $code .= 'var e="'.esc_js($ppco_err_id).'";'."\n";
869 938 $code .= 'var t="'.esc_js($ppco_token).'";'."\n";
870 939 $code .= 'var u="'.esc_js($ppco_endpoint).'";'."\n";
940 + //260928.1739 Bind to the SDK script actually emitted above and encode its URL as JavaScript, not HTML entities.
871 941 $code .= 'var ns="'.esc_js($ppco_sdk_ns).'";'."\n";
872 - $code .= 'var s="'.esc_js($ppco_sdk_src).'";'."\n";
942 + $code .= 'var sdkId="'.esc_js($ppco_sdk_id).'";'."\n";
943 + $code .= 'var s='.wp_json_encode($ppco_sdk_src, JSON_HEX_AMP | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT).';'."\n";
873 944 $code .= 'var cid="'.esc_js($paypal_invoice_input_value).'";'."\n";
874 - $code .= 'function showErr(m){try{var el=document.getElementById(e);if(el){el.style.display="block";el.innerHTML=m;}}catch(x){}}'."\n";
875 - $code .= 'function postTo(url, data){var f=document.createElement("form");f.method="post";f.action=url;for(var k in data){if(!data.hasOwnProperty(k))continue;var i=document.createElement("input");i.type="hidden";i.name=k;i.value=data[k];f.appendChild(i);}document.body.appendChild(f);f.submit();}'."\n";
945 + //260819.0042 Keep standalone Checkout feedback consistent with Pro-Forms while distinguishing cancellation from errors.
946 + $code .= 'function showMsg(m,t){try{var el=document.getElementById(e);if(el){el.style.display="block";el.innerHTML="<span class=\"ws-plugin--s2member-ppco-"+(t==="info"?"info":"error")+"\">"+m+"</span>";}}catch(x){}}function showErr(m){showMsg(m,"error");}function showInfo(m){showMsg(m,"info");}'."\n";
947 + $code .= 'function postTo(url, data){var f=document.createElement("form");f.method="post";f.acceptCharset="UTF-8";f.action=url;for(var k in data){if(!data.hasOwnProperty(k))continue;var i=document.createElement("input");i.type="hidden";i.name=k;i.value=data[k];f.appendChild(i);}document.body.appendChild(f);f.submit();}'."\n"; //260817 Keep signed PayPal Checkout returns in UTF-8.
876 948 $code .= 'function enc(o){var a=[];for(var k in o){if(!o.hasOwnProperty(k))continue;a.push(encodeURIComponent(k)+"="+encodeURIComponent(o[k]));}return a.join("&");}'."\n";
877 - $code .= 'function loadSdk(cb){var P=window[ns];if(P&&P.Buttons){cb(P);return;}var id=ns+"_sdk",tag=document.getElementById(id),done=false;function finish(){if(done)return;done=true;cb(window[ns]&&window[ns].Buttons?window[ns]:null);}function ok(){finish();}function fail(){finish();}if(tag){if(window[ns]&&window[ns].Buttons){finish();return;}if(tag.getAttribute("src")!==s){tag.setAttribute("src",s);}if(tag.readyState==="complete"||tag.readyState==="loaded"){setTimeout(finish,0);return;}tag.addEventListener("load",ok);tag.addEventListener("error",fail);setTimeout(finish,3500);return;}tag=document.createElement("script");tag.id=id;tag.setAttribute("data-namespace",ns);tag.src=s;tag.async=true;tag.onload=ok;tag.onerror=fail;(document.head||document.body||document.documentElement).appendChild(tag);setTimeout(finish,3500);}'."\n";
949 + $code .= 'function loadSdk(cb){var P=window[ns];if(P&&P.Buttons){cb(P);return;}var id=sdkId,tag=document.getElementById(id),done=false;function finish(){if(done)return;done=true;cb(window[ns]&&window[ns].Buttons?window[ns]:null);}function ok(){finish();}function fail(){finish();}if(tag){if(window[ns]&&window[ns].Buttons){finish();return;}if(tag.getAttribute("src")!==s){tag.setAttribute("src",s);}if(tag.readyState==="complete"||tag.readyState==="loaded"){setTimeout(finish,0);return;}tag.addEventListener("load",ok);tag.addEventListener("error",fail);setTimeout(finish,3500);return;}tag=document.createElement("script");tag.id=id;tag.setAttribute("data-namespace",ns);tag.src=s;tag.async=true;tag.onload=ok;tag.onerror=fail;(document.head||document.body||document.documentElement).appendChild(tag);setTimeout(finish,3500);}'."\n";
878 950 if($ppco_intent === 'subscription')
879 951 {
880 - $code .= 'function getPlanId(){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"get_plan_id",s2member_paypal_checkout_t:t})}).then(function(r){return r.json();}).then(function(res){if(res&&res.plan_id)return res.plan_id;throw(res&&res.error?res.error:"plan_get_failed");});}'."\n";
881 - $code .= 'var planId=null;'."\n";
882 - $code .= 'function createSubscription(data,actions){if(planId)return actions.subscription.create({plan_id:planId,custom_id:cid,application_context:{shipping_preference:"NO_SHIPPING"}});return getPlanId().then(function(pid){planId=pid;return actions.subscription.create({plan_id:planId,custom_id:cid,application_context:{shipping_preference:"NO_SHIPPING"}});});}'."\n";
883 - $code .= 'function onApprove(data){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"confirm_subscription",s2member_paypal_checkout_t:t,subscription_id:(data&&data.subscriptionID?data.subscriptionID:"")})}).then(function(r){return r.json();}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}throw(res&&res.error?res.error:"subscription_confirm_failed");}).catch(function(e){showErr("Subscription could not be completed. Please try again.");});}'."\n";
884 - $code .= 'function onCancel(){showErr("Subscription cancelled.");}'."\n";
952 + //260928.1540 Move Framework subscription creation onto the same server-side provider/idempotency path Pro-Forms use. Only the persisted ID reaches the PayPal SDK for buyer approval.
953 + $code .= 'function request(op,vars){var body={s2member_paypal_checkout_op:op,s2member_paypal_checkout_t:t};for(var k in (vars||{})){if(vars.hasOwnProperty(k))body[k]=vars[k];}return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc(body)}).then(function(r){return r.json();});}' . "\n";
954 + $code .= 'function waitForSubscription(n){return request("get_subscription_id").then(function(res){if(res&&res.subscription_id)return res.subscription_id;if(n>=20)throw "subscription_create_unresolved";return new Promise(function(resolve){setTimeout(resolve,1000);}).then(function(){return waitForSubscription(n+1);});});}' . "\n";
955 + $code .= 'function createSubscription(){return request("create_subscription").then(function(res){if(res&&res.subscription_id)return res.subscription_id;if(res&&res.recoverable)return waitForSubscription(0);throw(res ? (res.error || "subscription_create_failed") : "subscription_create_failed");});}' . "\n";
956 + //260928.1540 PayPal can report APPROVED before subscription ACTIVATED; poll the same backend until entitlement is confirmed or the activation webhook fulfills off-session.
957 + //260928.1739 WordPress rendered this inline JS with &#038;&#038; inside throw(res&&res.error), breaking the entire PayPal Button script. Use a ternary and || instead.
958 + $code .= 'function onApprove(data){var sid=data&&data.subscriptionID?data.subscriptionID:"";function finish(n){return request("confirm_subscription",{subscription_id:sid}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}if(res&&res.pending_activation&&n<25){return new Promise(function(resolve){setTimeout(resolve,1200);}).then(function(){return finish(n+1);});}throw(res ? (res.error || "subscription_confirm_failed") : "subscription_confirm_failed");});}return finish(0).catch(function(){showErr("Subscription could not be completed. Please try again.");});}' . "\n";
959 + $code .= 'function onCancel(){showInfo("Subscription cancelled.");}'."\n";
885 960 $code .= 'function onError(err){var m="PayPal error. Please try again.";try{if(err){if(typeof err==="string")m="PayPal error: "+err;else if(err.message)m="PayPal error: "+err.message;}}catch(x){}showErr(m);}'."\n";
886 961 $code .= 'function init(){loadSdk(function(P){var el=document.getElementById(d);if(!el){return;}if(el.getAttribute("data-s2m-ppco-rendered")==="1"){return;}if(!P||!P.Buttons){showErr("PayPal SDK failed to load.");return;}el.setAttribute("data-s2m-ppco-rendered","1");try{P.Buttons({fundingSource:P.FUNDING.PAYPAL,style:{layout:"vertical",tagline:false,height:40},createSubscription:createSubscription,onApprove:onApprove,onCancel:onCancel,onError:onError}).render("#"+d);}catch(x){el.removeAttribute("data-s2m-ppco-rendered");showErr("PayPal render failed.");}});}'."\n";
887 962 }
888 963 else
889 964 {
890 - $code .= 'function createOrder(){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"create_order",s2member_paypal_checkout_t:t})}).then(function(r){return r.json();}).then(function(res){if(res&&res.order_id)return res.order_id;throw(res&&res.error?res.error:"order_create_failed");});}'."\n";
891 - $code .= 'function onApprove(data){return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc({s2member_paypal_checkout_op:"capture_order",s2member_paypal_checkout_t:t,order_id:(data&&data.orderID?data.orderID:"")})}).then(function(r){return r.json();}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}throw(res&&res.error?res.error:"order_capture_failed");}).catch(function(e){showErr("Payment could not be completed. Please try again.");});}'."\n";
892 - $code .= 'function onCancel(){showErr("Payment cancelled.");}'."\n";
965 + $code .= 'function request(op,vars){var body={s2member_paypal_checkout_op:op,s2member_paypal_checkout_t:t};for(var k in (vars||{})){if(vars.hasOwnProperty(k))body[k]=vars[k];}return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:enc(body)}).then(function(r){return r.json();});}'."\n";
966 + $code .= 'function createOrder(){return request("create_order").then(function(res){if(res&&res.order_id)return res.order_id;throw(res ? (res.error || "order_create_failed") : "order_create_failed");});}'."\n";
967 + //260928.1703 A completed PayPal capture can reach the verified webhook even when the browser loses its response. Poll only our durable state and submit its saved signed handoff; never issue a second capture on uncertainty.
968 + $code .= 'function recoverOrder(n){return request("get_order_status").then(function(res){if(res&&res.fulfilled&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}if(n>=20)throw "order_confirmation_pending";return new Promise(function(resolve){setTimeout(resolve,1200);}).then(function(){return recoverOrder(n+1);});});}'."\n";
969 + $code .= 'function onApprove(data){var oid=data&&data.orderID?data.orderID:"";return request("capture_order",{order_id:oid}).catch(function(){return {recoverable:true};}).then(function(res){if(res&&res.rtn_url&&res.rtn_post){postTo(res.rtn_url,res.rtn_post);return;}if(res&&res.recoverable)return recoverOrder(0).catch(function(){showInfo("Payment confirmation is pending. Please do not pay again; check your email or contact support.");});showErr("Payment could not be confirmed. Please contact support before trying again.");});}'."\n";
970 + $code .= 'function onCancel(){showInfo("Payment cancelled.");}'."\n";
893 971 $code .= 'function onError(err){var m="PayPal error. Please try again.";try{if(err){if(typeof err==="string")m="PayPal error: "+err;else if(err.message)m="PayPal error: "+err.message;}}catch(x){}showErr(m);}'."\n";
894 972 $code .= 'function init(){loadSdk(function(P){var el=document.getElementById(d);if(!el){return;}if(el.getAttribute("data-s2m-ppco-rendered")==="1"){return;}if(!P||!P.Buttons){showErr("PayPal SDK failed to load.");return;}el.setAttribute("data-s2m-ppco-rendered","1");try{P.Buttons({fundingSource:P.FUNDING.PAYPAL,style:{layout:"vertical",tagline:false,height:40},createOrder:createOrder,onApprove:onApprove,onCancel:onCancel,onError:onError}).render("#"+d);}catch(x){el.removeAttribute("data-s2m-ppco-rendered");showErr("PayPal render failed.");}});}'."\n";
895 973 }
896 974 $code .= 'if(document.readyState==="complete"){init();}else{window.addEventListener("load",init);}'."\n";