| @@ -120,10 +120,14 @@ | ||
| 120 | 120 | |
| 121 | 121 | $postvars = self::paypal_postvars_utf8($postvars); |
| 122 | 122 | $endpoint = ($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["paypal_sandbox"]) ? "www.sandbox.paypal.com" : "www.paypal.com"; |
| 123 | 123 | |
| 124 | + //260927.2250 Browser PayPal Returns must use the transaction-bound Checkout handoff above; never let the reusable server-to-server proxy credential authenticate them. | |
| 125 | + if(!empty($_GET["s2member_paypal_return"]) && !empty($_REQUEST["s2member_paypal_proxy"]) && is_string($_REQUEST["s2member_paypal_proxy"]) && $_REQUEST["s2member_paypal_proxy"] === "paypal") | |
| 126 | + return false; | |
| 127 | + | |
| 124 | 128 | //260909.0411 Normalize proxy verification input types and use the standard constant-time comparison helper. |
| 125 | - if(!empty($_REQUEST["s2member_paypal_proxy"]) && is_string($_REQUEST["s2member_paypal_proxy"]) && !empty($_REQUEST["s2member_paypal_proxy_verification"]) && is_string($_REQUEST["s2member_paypal_proxy_verification"]) && is_string($proxy_verification_key = c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen()) && hash_equals($proxy_verification_key, $_REQUEST["s2member_paypal_proxy_verification"])) | |
| 129 | + else if(!empty($_REQUEST["s2member_paypal_proxy"]) && is_string($_REQUEST["s2member_paypal_proxy"]) && !empty($_REQUEST["s2member_paypal_proxy_verification"]) && is_string($_REQUEST["s2member_paypal_proxy_verification"]) && is_string($proxy_verification_key = c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen()) && hash_equals($proxy_verification_key, $_REQUEST["s2member_paypal_proxy_verification"])) | |
| 126 | 130 | return apply_filters("ws_plugin__s2member_paypal_postvars", array_merge($postvars, array("proxy_verified" => $_REQUEST["s2member_paypal_proxy"])), get_defined_vars()); |
| 127 | 131 | |
| 128 | 132 | else if(empty($_POST) && !empty($_GET["s2member_paypal_proxy"]) && !empty($_GET["s2member_paypal_proxy_verification"]) && c_ws_plugin__s2member_utils_urls::s2member_sig_ok($_SERVER["REQUEST_URI"], false, false, "s2member_paypal_proxy_verification")) |
| 129 | 133 | return apply_filters("ws_plugin__s2member_paypal_postvars", array_merge($postvars, array("proxy_verified" => $_GET["s2member_paypal_proxy"])), get_defined_vars()); |
| @@ -1528,13 +1532,76 @@ | ||
| 1528 | 1532 | if(strpos($invoice, 's2mpf-') === 0) |
| 1529 | 1533 | $gateway_checkout_id = substr($invoice, strlen('s2mpf-')); |
| 1530 | 1534 | else if(strpos($invoice, 's2msp-') === 0) |
| 1531 | 1535 | $gateway_checkout_id = substr($invoice, strlen('s2msp-')); |
| 1536 | + else if(strpos($invoice, 's2mb-') === 0) //260928.1515 Standalone Framework buttons use their own invoice namespace, separate from Pro-Form account preparation. | |
| 1537 | + $gateway_checkout_id = substr($invoice, strlen('s2mb-')); | |
| 1532 | 1538 | |
| 1533 | 1539 | return c_ws_plugin__s2member_gateway_checkouts::valid_id($gateway_checkout_id) ? $gateway_checkout_id : ''; |
| 1534 | 1540 | } |
| 1535 | 1541 | |
| 1536 | 1542 | /** |
| 1543 | + * Starts or resumes a standalone Framework PayPal Checkout button using shared durable state. | |
| 1544 | + * | |
| 1545 | + * @since 260928.1520 | |
| 1546 | + * | |
| 1547 | + * @param array $token Verified, signed standalone button purchase token. | |
| 1548 | + * @param bool $create_allowed True only before starting provider work. | |
| 1549 | + * @return array Operation result containing ok and error. | |
| 1550 | + */ | |
| 1551 | + public static function paypal_checkout_button_gateway_checkout_prepare($token = array(), $create_allowed = FALSE) | |
| 1552 | + { | |
| 1553 | + $invoice = !empty($token['invoice']) ? (string)$token['invoice'] : ''; | |
| 1554 | + if(strpos($invoice, 's2mb-') !== 0) | |
| 1555 | + return array('ok' => TRUE, 'coordinator' => FALSE, 'error' => ''); // Existing in-flight button tokens and Pro-Forms use their established paths. | |
| 1556 | + | |
| 1557 | + $id = self::paypal_checkout_gateway_checkout_id_from_invoice($invoice); | |
| 1558 | + $browser_token = !empty($token['gateway_checkout_token']) ? (string)$token['gateway_checkout_token'] : ''; | |
| 1559 | + if(!$id || empty($token['gateway_checkout_id']) || !hash_equals($id, (string)$token['gateway_checkout_id']) | |
| 1560 | + || !c_ws_plugin__s2member_gateway_checkouts::browser_token_verify($id, $browser_token)) | |
| 1561 | + return array('ok' => FALSE, 'coordinator' => TRUE, 'error' => 'gateway_checkout_identity_invalid'); | |
| 1562 | + | |
| 1563 | + $operation = (!empty($token['rr']) && strtoupper((string)$token['rr']) !== 'BN') ? 'subscription' : 'payment'; | |
| 1564 | + $purchase_terms = (array)$token; | |
| 1565 | + unset($purchase_terms['exp'], $purchase_terms['gateway_checkout_token']); //260928.1520 Token renewal does not alter the underlying purchase contract. | |
| 1566 | + $fingerprint = c_ws_plugin__s2member_gateway_checkouts::purchase_fingerprint($purchase_terms); | |
| 1567 | + | |
| 1568 | + if($create_allowed) | |
| 1569 | + { | |
| 1570 | + //260928.1705 Do not rewrite a bound option on every retry: create_or_resume() may otherwise overwrite provider/fulfillment updates committed concurrently by a webhook. | |
| 1571 | + $state = c_ws_plugin__s2member_gateway_checkouts::load_state_uncached($id); | |
| 1572 | + if(!$state) | |
| 1573 | + $state = c_ws_plugin__s2member_gateway_checkouts::create_or_resume('paypal_checkout', $operation, $id, $browser_token, $fingerprint, get_current_user_id()); | |
| 1574 | + else if(!empty($state['user_id']) && (int)$state['user_id'] !== (int)get_current_user_id()) | |
| 1575 | + return array('ok' => FALSE, 'coordinator' => TRUE, 'error' => 'gateway_checkout_user_mismatch'); | |
| 1576 | + } | |
| 1577 | + else | |
| 1578 | + $state = c_ws_plugin__s2member_gateway_checkouts::load_state_uncached($id); | |
| 1579 | + | |
| 1580 | + //260928.1520 Reject a checkout returned under a replacement identity: the verified button token and PayPal invoice must keep pointing to the same durable record. | |
| 1581 | + if(!$state || !hash_equals($id, (string)$state['id']) || (string)$state['gateway'] !== 'paypal_checkout' | |
| 1582 | + || (string)$state['operation'] !== $operation || !hash_equals($fingerprint, (string)$state['purchase_fingerprint'])) | |
| 1583 | + return array('ok' => FALSE, 'coordinator' => TRUE, 'error' => 'gateway_checkout_mismatch'); | |
| 1584 | + | |
| 1585 | + $private = c_ws_plugin__s2member_gateway_checkouts::private_context_get($id); | |
| 1586 | + if($private === FALSE) | |
| 1587 | + return array('ok' => FALSE, 'coordinator' => TRUE, 'error' => 'gateway_checkout_private_context_invalid'); | |
| 1588 | + | |
| 1589 | + if(empty($private['paypal_checkout']['token'])) | |
| 1590 | + { | |
| 1591 | + if(!$create_allowed) | |
| 1592 | + return array('ok' => FALSE, 'coordinator' => TRUE, 'error' => 'gateway_checkout_purchase_context_missing'); | |
| 1593 | + $private = (array)$private; | |
| 1594 | + $private['paypal_checkout'] = !empty($private['paypal_checkout']) && is_array($private['paypal_checkout']) ? $private['paypal_checkout'] : array(); | |
| 1595 | + //260928.1520 The first provider operation durably stores the authenticated purchase token for webhook-only fulfillment. No password/card data is stored. | |
| 1596 | + $private['paypal_checkout']['token'] = $token; | |
| 1597 | + if(!c_ws_plugin__s2member_gateway_checkouts::private_context_set($id, $private)) | |
| 1598 | + return array('ok' => FALSE, 'coordinator' => TRUE, 'error' => 'gateway_checkout_purchase_context_save_failed'); | |
| 1599 | + } | |
| 1600 | + return array('ok' => TRUE, 'coordinator' => TRUE, 'error' => '', 'gateway_checkout_id' => $id); | |
| 1601 | + } | |
| 1602 | + | |
| 1603 | + /** | |
| 1537 | 1604 | * Creates a PayPal Checkout order for one-time (Buy Now) purchases. |
| 1538 | 1605 | * |
| 1539 | 1606 | * This must be server-side to prevent client-side manipulation of amount, item_number, |
| 1540 | 1607 | * custom fields, etc. The resulting order id is returned to the JS SDK or used for |
| @@ -1653,9 +1720,11 @@ | ||
| 1653 | 1720 | return array('__error' => 'gateway_checkout_private_context_failed'); |
| 1654 | 1721 | $private_context = (array)$private_context; |
| 1655 | 1722 | $private_context['paypal_checkout'] = !empty($private_context['paypal_checkout']) && is_array($private_context['paypal_checkout']) ? $private_context['paypal_checkout'] : array(); |
| 1656 | 1723 | //260902.0635 Save the validated token before contacting PayPal so a later capture webhook has enough trusted server-side context to finish an interrupted browser checkout. |
| 1657 | - $private_context['paypal_checkout']['token'] = $token; | |
| 1724 | + //260928.1615 An anchor/url checkout temporarily substitutes PayPal's internal approval-return URL for provider creation; keep the canonical, previously validated button token so a capture webhook returns the buyer to the original success page. | |
| 1725 | + if(strpos($invoice, 's2mb-') !== 0 || empty($private_context['paypal_checkout']['token'])) | |
| 1726 | + $private_context['paypal_checkout']['token'] = $token; | |
| 1658 | 1727 | if(!c_ws_plugin__s2member_gateway_checkouts::private_context_set($gateway_checkout_id, $private_context)) |
| 1659 | 1728 | return array('__error' => 'gateway_checkout_private_context_failed'); |
| 1660 | 1729 | |
| 1661 | 1730 | $context = !empty($gateway_checkout['context']) && is_array($gateway_checkout['context']) ? $gateway_checkout['context'] : array(); |
| @@ -2154,12 +2223,10 @@ | ||
| 2154 | 2223 | $private_context['paypal_checkout']['fulfillment_result'] = $result; |
| 2155 | 2224 | if(!c_ws_plugin__s2member_gateway_checkouts::private_context_set($gateway_checkout_id, $private_context)) |
| 2156 | 2225 | return array('ok' => FALSE, 'error' => 'gateway_checkout_private_context_failed'); |
| 2157 | 2226 | |
| 2158 | - $gateway_ids = !empty($gateway_checkout['gateway_ids']) && is_array($gateway_checkout['gateway_ids']) ? $gateway_checkout['gateway_ids'] : array(); | |
| 2159 | - $gateway_ids['order_id'] = $order_id; | |
| 2160 | - $gateway_ids['capture_id'] = $pu_cap_id; | |
| 2161 | - if(!c_ws_plugin__s2member_gateway_checkouts::update($gateway_checkout_id, array('gateway_ids' => $gateway_ids, 'gateway_status' => 'CAPTURE_COMPLETED', 'fulfillment_status' => 'fulfilled'))) | |
| 2227 | + //260928.1705 Final fulfillment must patch the latest checkout version: a concurrent webhook/browser context write must not be lost or downgrade the terminal fulfilled state. | |
| 2228 | + if(!c_ws_plugin__s2member_gateway_checkouts::patch($gateway_checkout_id, array('gateway_ids' => array('order_id' => $order_id, 'capture_id' => $pu_cap_id), 'gateway_status' => 'CAPTURE_COMPLETED', 'fulfillment_status' => 'fulfilled'))) | |
| 2162 | 2229 | return array('ok' => FALSE, 'error' => 'gateway_checkout_save_failed'); |
| 2163 | 2230 | |
| 2164 | 2231 | return array_merge(array('ok' => TRUE, 'processed' => !empty($notify_result['processed']), 'duplicate' => !empty($notify_result['duplicate'])), $result); |
| 2165 | 2232 | } |
| @@ -2283,9 +2350,9 @@ | ||
| 2283 | 2350 | { |
| 2284 | 2351 | $invoice = trim((string)$invoice); |
| 2285 | 2352 | $subscription_id = trim((string)$subscription_id); |
| 2286 | 2353 | $status = strtoupper(trim((string)$status)); |
| 2287 | - $gateway_checkout_id = (strpos($invoice, 's2mpf-') === 0) ? substr($invoice, strlen('s2mpf-')) : ''; | |
| 2354 | + $gateway_checkout_id = self::paypal_checkout_gateway_checkout_id_from_invoice($invoice); //260928.1515 Recover both Pro-Forms and standalone Framework button subscriptions by their signed invoice identity. | |
| 2288 | 2355 | |
| 2289 | 2356 | if(!$subscription_id || !c_ws_plugin__s2member_gateway_checkouts::valid_id($gateway_checkout_id)) |
| 2290 | 2357 | return array('handled' => false, 'ok' => false, 'recovered' => false, 'error' => 'not_coordinator_checkout'); |
| 2291 | 2358 | |
| @@ -2332,8 +2399,115 @@ | ||
| 2332 | 2399 | { |
| 2333 | 2400 | c_ws_plugin__s2member_gateway_checkouts::processing_unlock($gateway_checkout_id, $lock); |
| 2334 | 2401 | } |
| 2335 | 2402 | } |
| 2403 | + | |
| 2404 | + /** | |
| 2405 | + * Completes an approved standalone Framework button subscription from the same | |
| 2406 | + * authoritative PayPal resource whether invoked by browser or verified webhook. | |
| 2407 | + * | |
| 2408 | + * @since 260928.1530 | |
| 2409 | + */ | |
| 2410 | + public static function paypal_checkout_button_subscription_fulfill($subscription = array(), $token = array(), $via = 'webhook') | |
| 2411 | + { | |
| 2412 | + $invoice = !empty($token['invoice']) ? (string)$token['invoice'] : ''; | |
| 2413 | + $id = self::paypal_checkout_gateway_checkout_id_from_invoice($invoice); | |
| 2414 | + if(!$id || strpos($invoice, 's2mb-') !== 0 || empty($token['gateway_checkout_id']) || !hash_equals($id, (string)$token['gateway_checkout_id'])) | |
| 2415 | + return array('ok' => FALSE, 'error' => 'gateway_checkout_identity_invalid'); | |
| 2416 | + | |
| 2417 | + $state = c_ws_plugin__s2member_gateway_checkouts::load_state($id); | |
| 2418 | + if(!$state || (string)$state['gateway'] !== 'paypal_checkout' || (string)$state['operation'] !== 'subscription') | |
| 2419 | + return array('ok' => FALSE, 'error' => 'gateway_checkout_invalid'); | |
| 2420 | + | |
| 2421 | + $subscription_id = !empty($subscription['id']) ? (string)$subscription['id'] : ''; | |
| 2422 | + $status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : ''; | |
| 2423 | + $custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : ''; | |
| 2424 | + if(!$subscription_id || !$custom_id || !hash_equals($invoice, $custom_id)) | |
| 2425 | + return array('ok' => FALSE, 'error' => 'subscription_purchase_identity_mismatch'); | |
| 2426 | + | |
| 2427 | + $expected_plan = self::paypal_checkout_plan_get_id($token); | |
| 2428 | + if(!$expected_plan || empty($subscription['plan_id']) || !hash_equals((string)$expected_plan, (string)$subscription['plan_id'])) | |
| 2429 | + return array('ok' => FALSE, 'error' => 'subscription_plan_mismatch'); | |
| 2430 | + | |
| 2431 | + $is_single_cycle = isset($token['rr']) && (string)$token['rr'] === '0'; | |
| 2432 | + $last_payment_amount = isset($subscription['billing_info']['last_payment']['amount']['value']) ? (string)$subscription['billing_info']['last_payment']['amount']['value'] : ''; | |
| 2433 | + $last_payment_currency = !empty($subscription['billing_info']['last_payment']['amount']['currency_code']) ? strtoupper((string)$subscription['billing_info']['last_payment']['amount']['currency_code']) : ''; | |
| 2434 | + //260928.1703 An immediately EXPIRED single-cycle subscription is paid only when PayPal's reported last payment matches the signed price and currency, not merely when a payment field exists. | |
| 2435 | + $last_paid = ($last_payment_amount !== '' && is_numeric($last_payment_amount) && isset($token['amount']) | |
| 2436 | + && number_format((float)$last_payment_amount, 2, '.', '') === number_format((float)$token['amount'], 2, '.', '') | |
| 2437 | + && !empty($token['cc']) && $last_payment_currency === strtoupper((string)$token['cc'])); | |
| 2438 | + if($status !== 'ACTIVE' && !($is_single_cycle && $status === 'EXPIRED' && $last_paid)) | |
| 2439 | + return in_array($status, array('APPROVAL_PENDING', 'APPROVED'), TRUE) | |
| 2440 | + ? array('ok' => FALSE, 'pending_activation' => TRUE, 'error' => 'pending_activation', 'status' => $status) | |
| 2441 | + : array('ok' => FALSE, 'error' => 'subscription_status_invalid', 'status' => $status); | |
| 2442 | + | |
| 2443 | + //260928.1530 Bind the real subscription ID before fulfillment so a second event/browser request cannot attach a different provider subscription to this purchase. | |
| 2444 | + $recovery = self::paypal_checkout_subscription_gateway_checkout_recover($invoice, $subscription_id, $status); | |
| 2445 | + if(empty($recovery['handled']) || empty($recovery['ok'])) | |
| 2446 | + { | |
| 2447 | + //260928.1608 An independent CREATED/ACTIVATED webhook can own the coordinator lock briefly; the browser should poll rather than report a permanent checkout failure. | |
| 2448 | + //260928.1703 A redirect return also competes with CREATED/ACTIVATED webhook recovery; let it retry the signed return instead of displaying a spurious failure. | |
| 2449 | + if(in_array($via, array('browser', 'return'), TRUE) && !empty($recovery['error']) && $recovery['error'] === 'gateway_checkout_busy') | |
| 2450 | + return array('ok' => FALSE, 'pending_activation' => TRUE, 'status' => $status, 'error' => 'gateway_checkout_busy'); | |
| 2451 | + return array('ok' => FALSE, 'error' => !empty($recovery['error']) ? $recovery['error'] : 'subscription_recovery_failed'); | |
| 2452 | + } | |
| 2453 | + | |
| 2454 | + $private = c_ws_plugin__s2member_gateway_checkouts::private_context_get($id); | |
| 2455 | + if(!is_array($private) || empty($private['paypal_checkout']['token']) || !is_array($private['paypal_checkout']['token'])) | |
| 2456 | + return array('ok' => FALSE, 'error' => 'gateway_checkout_purchase_context_missing'); | |
| 2457 | + $stored_token = $private['paypal_checkout']['token']; | |
| 2458 | + if(empty($stored_token['invoice']) || !hash_equals($invoice, (string)$stored_token['invoice']) || empty($stored_token['item_number'])) | |
| 2459 | + return array('ok' => FALSE, 'error' => 'gateway_checkout_purchase_context_mismatch'); | |
| 2460 | + | |
| 2461 | + $state = c_ws_plugin__s2member_gateway_checkouts::load_state($id); | |
| 2462 | + if($state && (string)$state['fulfillment_status'] === 'fulfilled' && !empty($private['paypal_checkout']['fulfillment_result'])) | |
| 2463 | + return array_merge(array('ok' => TRUE, 'processed' => FALSE, 'duplicate' => TRUE), $private['paypal_checkout']['fulfillment_result']); | |
| 2464 | + | |
| 2465 | + $paypal = array( | |
| 2466 | + 'txn_type' => 'subscr_signup', 'payment_status' => 'Completed', 'subscr_gateway' => 'paypal', | |
| 2467 | + 'txn_id' => $subscription_id, 'subscr_id' => $subscription_id, 'subscr_baid' => $subscription_id, 'subscr_cid' => $subscription_id, | |
| 2468 | + 'mc_gross' => (string)$stored_token['amount'], 'mc_currency' => strtoupper((string)$stored_token['cc']), | |
| 2469 | + 'period1' => (!empty($stored_token['tp']) && !empty($stored_token['tt'])) ? ((string)$stored_token['tp'].' '.strtoupper((string)$stored_token['tt'])) : '0 D', | |
| 2470 | + 'mc_amount1' => (!empty($stored_token['tp']) && !empty($stored_token['tt'])) ? (string)$stored_token['ta'] : '0.00', | |
| 2471 | + 'period3' => ((string)$stored_token['rp'].' '.strtoupper((string)$stored_token['rt'])), | |
| 2472 | + 'mc_amount3' => (string)$stored_token['amount'], | |
| 2473 | + 'recurring' => ((isset($stored_token['rr']) && (string)$stored_token['rr'] === '1') ? '1' : '0'), | |
| 2474 | + 'invoice' => $invoice, 'custom' => (string)$stored_token['custom'], | |
| 2475 | + 'item_name' => (string)$stored_token['item_name'], 'item_number' => (string)$stored_token['item_number'], | |
| 2476 | + 'payer_email' => !empty($subscription['subscriber']['email_address']) ? (string)$subscription['subscriber']['email_address'] : '', | |
| 2477 | + 'first_name' => !empty($subscription['subscriber']['name']['given_name']) ? (string)$subscription['subscriber']['name']['given_name'] : '', | |
| 2478 | + 'last_name' => !empty($subscription['subscriber']['name']['surname']) ? (string)$subscription['subscriber']['name']['surname'] : '', | |
| 2479 | + 'option_name1' => (string)$stored_token['on0'], 'option_selection1' => (string)$stored_token['os0'], | |
| 2480 | + 'option_name2' => (string)$stored_token['on1'], 'option_selection2' => (string)$stored_token['os1'], | |
| 2481 | + ); | |
| 2482 | + | |
| 2483 | + $notify_result = self::paypal_checkout_notify_once($paypal, 's2m_ppco_subscr_done_'.md5($subscription_id)); | |
| 2484 | + if(empty($notify_result['ok'])) | |
| 2485 | + return array('ok' => FALSE, 'error' => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed'); | |
| 2486 | + | |
| 2487 | + $return_url = add_query_arg('s2member_paypal_proxy', 'paypal', (string)$stored_token['return']); | |
| 2488 | + $return_post = array_merge($paypal, array('s2member_paypal_proxy' => 'paypal', 's2member_paypal_proxy_use' => 'paypal_checkout')); | |
| 2489 | + $handoff = self::paypal_checkout_return_handoff_create($return_post); | |
| 2490 | + if(!$handoff) | |
| 2491 | + return array('ok' => FALSE, 'error' => 'return_handoff_failed'); | |
| 2492 | + $return_post['s2member_paypal_checkout_handoff'] = $handoff; | |
| 2493 | + $result = array('rtn_url' => $return_url, 'rtn_post' => $return_post, 'subscription_id' => $subscription_id); | |
| 2494 | + | |
| 2495 | + $private['paypal_checkout']['fulfillment_result'] = $result; | |
| 2496 | + if(!c_ws_plugin__s2member_gateway_checkouts::private_context_set($id, $private)) | |
| 2497 | + return array('ok' => FALSE, 'error' => 'gateway_checkout_private_context_save_failed'); | |
| 2498 | + if(!c_ws_plugin__s2member_gateway_checkouts::patch($id, array('gateway_ids' => array('subscription_id' => $subscription_id), 'gateway_status' => $status, 'fulfillment_status' => 'fulfilled'))) | |
| 2499 | + return array('ok' => FALSE, 'error' => 'gateway_checkout_save_failed'); | |
| 2500 | + | |
| 2501 | + //260928.1530 Preserve button upgrade semantics: only the request that processed Notify may cancel the old subscription, never a duplicate callback. | |
| 2502 | + $old_id = !empty($stored_token['old__subscr_id']) ? (string)$stored_token['old__subscr_id'] : ''; | |
| 2503 | + if(!empty($notify_result['processed']) && $old_id && $old_id !== $subscription_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', TRUE, array('old__subscr_id' => $old_id, 'subscr_id' => $subscription_id))) | |
| 2504 | + c_ws_plugin__s2member_utilities::cancel_gateway_subscription(!empty($stored_token['old__subscr_gateway']) ? (string)$stored_token['old__subscr_gateway'] : '', $old_id, | |
| 2505 | + !empty($stored_token['old__subscr_baid']) ? (string)$stored_token['old__subscr_baid'] : '', !empty($stored_token['old__subscr_cid']) ? (string)$stored_token['old__subscr_cid'] : '', | |
| 2506 | + !empty($stored_token['old__ipn_signup_vars']) && is_array($stored_token['old__ipn_signup_vars']) ? $stored_token['old__ipn_signup_vars'] : array()); | |
| 2507 | + | |
| 2508 | + return array_merge(array('ok' => TRUE, 'processed' => !empty($notify_result['processed']), 'duplicate' => !empty($notify_result['duplicate'])), $result); | |
| 2509 | + } | |
| 2336 | 2510 | |
| 2337 | 2511 | /** |
| 2338 | 2512 | * Creates a PayPal Checkout subscription server-side. |
| 2339 | 2513 | * |