-
-
Notifications
You must be signed in to change notification settings - Fork 70
Fix PayPal checkout loop and improve error handling #305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
superdav42
wants to merge
1
commit into
main
Choose a base branch
from
fix/paypal-checkout-loop-193
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+192
−22
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -420,7 +420,7 @@ | |
| * @param \WP_Ultimo\Checkout\Cart $cart The cart object. | ||
| * @param string $type The checkout type. Can be 'new', 'retry', 'upgrade', 'downgrade', 'addon'. | ||
| * @return void | ||
| */ | ||
|
Check failure on line 423 in inc/gateways/class-paypal-gateway.php
|
||
| public function process_checkout($payment, $membership, $customer, $cart, $type): void { | ||
| /* | ||
| * To make our lives easier, let's | ||
|
|
@@ -649,7 +649,7 @@ | |
| * | ||
| * Redirect to the PayPal checkout URL. | ||
| */ | ||
| wp_redirect($this->checkout_url . $body['TOKEN']); | ||
|
Check warning on line 652 in inc/gateways/class-paypal-gateway.php
|
||
|
|
||
| exit; | ||
| } | ||
|
|
@@ -830,8 +830,39 @@ | |
| */ | ||
| $details = $this->get_checkout_details(wu_request('token')); | ||
|
|
||
| if (empty($details)) { | ||
| wp_die(esc_html__('PayPal token no longer valid.', 'ultimate-multisite')); | ||
| /* | ||
| * Check if we got a WP_Error back from PayPal (e.g., invalid credentials, expired token). | ||
| */ | ||
| if (is_wp_error($details)) { | ||
| wu_log_add('paypal', 'PayPal confirmation failed: ' . $details->get_error_message(), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html( | ||
| sprintf( | ||
| // translators: %s is the PayPal error message. | ||
| __('PayPal Error: %s', 'ultimate-multisite'), | ||
| $details->get_error_message() | ||
| ) | ||
| ), | ||
| esc_html__('PayPal Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| if (empty($details) || ! is_array($details)) { | ||
| wu_log_add('paypal', 'PayPal confirmation failed: token no longer valid or empty response', LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html__('PayPal token no longer valid. Please try again.', 'ultimate-multisite'), | ||
| esc_html__('PayPal Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| /* | ||
|
|
@@ -845,7 +876,16 @@ | |
| * Bail. | ||
| */ | ||
| if (empty($payment)) { | ||
| wp_die(esc_html__('Pending payment does not exist.', 'ultimate-multisite')); | ||
| wu_log_add('paypal', 'PayPal confirmation failed: pending payment not found', LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html__('Pending payment does not exist. Please try again or contact support.', 'ultimate-multisite'), | ||
| esc_html__('Payment Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| /* | ||
|
|
@@ -857,7 +897,16 @@ | |
| $original_cart = $payment->get_meta('wu_original_cart'); | ||
|
|
||
| if (empty($original_cart)) { | ||
| wp_die(esc_html__('Original cart does not exist.', 'ultimate-multisite')); | ||
| wu_log_add('paypal', 'PayPal confirmation failed: original cart not found for payment ' . $payment->get_id(), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html__('Original cart does not exist. Please try again or contact support.', 'ultimate-multisite'), | ||
| esc_html__('Cart Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| /* | ||
|
|
@@ -869,9 +918,16 @@ | |
| $is_recurring = $original_cart->has_recurring(); | ||
|
|
||
| if (empty($membership) || empty($customer)) { | ||
| $error = new \WP_Error('no-membership', esc_html__('Missing membership or customer data.', 'ultimate-multisite')); | ||
|
|
||
| wp_die($error); // phpcs:ignore WordPress.Security.EscapeOutput | ||
| wu_log_add('paypal', 'PayPal confirmation failed: missing membership or customer for payment ' . $payment->get_id(), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html__('Missing membership or customer data. Please try again or contact support.', 'ultimate-multisite'), | ||
| esc_html__('Data Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| if ($should_auto_renew && $is_recurring) { | ||
|
|
@@ -1274,6 +1330,8 @@ | |
| $args['TOTALBILLINGCYCLES'] = $membership->get_billing_cycles() - $membership->get_times_billed(); | ||
| } | ||
|
|
||
| wu_log_add('paypal', sprintf('Creating recurring payment profile for payment #%d', $payment->get_id())); | ||
|
|
||
| $request = wp_remote_post( | ||
| $this->api_endpoint, | ||
| [ | ||
|
|
@@ -1284,7 +1342,16 @@ | |
| ); | ||
|
|
||
| if (is_wp_error($request)) { | ||
| wp_die(esc_html($request->get_error_message())); | ||
| wu_log_add('paypal', 'CreateRecurringPaymentsProfile request failed: ' . $request->get_error_message(), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html($request->get_error_message()), | ||
| esc_html__('PayPal Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| $body = wp_remote_retrieve_body($request); | ||
|
|
@@ -1300,8 +1367,21 @@ | |
| wp_parse_str($body, $body); | ||
| } | ||
|
|
||
| if ('failure' === strtolower((string) $body['ACK'])) { | ||
| wp_die(esc_html($body['L_LONGMESSAGE0']), esc_html($body['L_ERRORCODE0'])); | ||
| if ('failure' === strtolower((string) $body['ACK']) || 'failurewithwarning' === strtolower((string) $body['ACK'])) { | ||
| $error_code = $body['L_ERRORCODE0'] ?? 'unknown'; | ||
| $error_message = $body['L_LONGMESSAGE0'] ?? __('Unknown PayPal error', 'ultimate-multisite'); | ||
|
|
||
| wu_log_add('paypal', sprintf('CreateRecurringPaymentsProfile failed with error %s: %s', $error_code, $error_message), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| // translators: %1$s is the PayPal error code, %2$s is the error message. | ||
| esc_html(sprintf(__('PayPal Error (%1$s): %2$s', 'ultimate-multisite'), $error_code, $error_message)), | ||
| esc_html__('PayPal Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } else { | ||
| /* | ||
| * We were successful, let's update | ||
|
|
@@ -1313,6 +1393,8 @@ | |
| $transaction_id = $body['TRANSACTIONID'] ?? ''; | ||
| $profile_status = $body['PROFILESTATUS'] ?? ''; | ||
|
|
||
| wu_log_add('paypal', sprintf('CreateRecurringPaymentsProfile successful. Profile ID: %s, Transaction ID: %s, Status: %s', $body['PROFILEID'] ?? 'N/A', $transaction_id ?: 'N/A', $profile_status ?: 'N/A')); | ||
|
|
||
| // If TRANSACTIONID is not passed we need to wait for webhook | ||
| $payment_status = Payment_Status::PENDING; | ||
|
|
||
|
|
@@ -1385,12 +1467,14 @@ | |
| exit; | ||
| } | ||
| } else { | ||
| wu_log_add('paypal', sprintf('CreateRecurringPaymentsProfile received unexpected response: HTTP %d %s', $code, $message), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html__('Something has gone wrong, please try again', 'ultimate-multisite'), | ||
| esc_html__('Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '401', | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
@@ -1445,6 +1529,8 @@ | |
| 'BUTTONSOURCE' => 'WP_Ultimo', | ||
| ]; | ||
|
|
||
| wu_log_add('paypal', sprintf('Processing single payment for payment #%d', $payment->get_id())); | ||
|
|
||
| $request = wp_remote_post( | ||
| $this->api_endpoint, | ||
| [ | ||
|
|
@@ -1463,16 +1549,38 @@ | |
| $message = wp_remote_retrieve_response_message($request); | ||
|
|
||
| if (is_wp_error($request)) { | ||
| wp_die(esc_html($request->get_error_message())); | ||
| wu_log_add('paypal', 'DoExpressCheckoutPayment request failed: ' . $request->get_error_message(), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html($request->get_error_message()), | ||
| esc_html__('PayPal Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| if (200 === absint($code) && 'OK' === $message) { | ||
| if (is_string($body)) { | ||
| wp_parse_str($body, $body); | ||
| } | ||
|
|
||
| if ('failure' === strtolower((string) $body['ACK'])) { | ||
| wp_die(esc_html($body['L_LONGMESSAGE0']), esc_html($body['L_ERRORCODE0'])); | ||
| if ('failure' === strtolower((string) $body['ACK']) || 'failurewithwarning' === strtolower((string) $body['ACK'])) { | ||
| $error_code = $body['L_ERRORCODE0'] ?? 'unknown'; | ||
| $error_message = $body['L_LONGMESSAGE0'] ?? __('Unknown PayPal error', 'ultimate-multisite'); | ||
|
|
||
| wu_log_add('paypal', sprintf('DoExpressCheckoutPayment failed with error %s: %s', $error_code, $error_message), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| // translators: %1$s is the PayPal error code, %2$s is the error message. | ||
| esc_html(sprintf(__('PayPal Error (%1$s): %2$s', 'ultimate-multisite'), $error_code, $error_message)), | ||
| esc_html__('PayPal Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } else { | ||
| /* | ||
| * We were successful, let's update | ||
|
|
@@ -1530,6 +1638,8 @@ | |
| $membership->save(); | ||
| } | ||
|
|
||
| wu_log_add('paypal', sprintf('DoExpressCheckoutPayment successful. Transaction ID: %s', $transaction_id)); | ||
|
|
||
| $this->payment = $payment; | ||
| $redirect_url = $this->get_return_url(); | ||
|
|
||
|
|
@@ -1538,12 +1648,14 @@ | |
| exit; | ||
| } | ||
| } else { | ||
| wu_log_add('paypal', sprintf('DoExpressCheckoutPayment received unexpected response: HTTP %d %s', $code, $message), LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html__('Something has gone wrong, please try again', 'ultimate-multisite'), | ||
| esc_html__('Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '401', | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
@@ -1553,9 +1665,9 @@ | |
| * Display the confirmation form. | ||
| * | ||
| * @since 2.1 | ||
| * @return string | ||
| * @return void | ||
| */ | ||
| public function confirmation_form() { | ||
| public function confirmation_form(): void { | ||
|
|
||
| $token = sanitize_text_field(wu_request('token')); | ||
|
|
||
|
|
@@ -1565,21 +1677,62 @@ | |
| $error = is_wp_error($checkout_details) ? $checkout_details->get_error_message() : __('Invalid response code from PayPal', 'ultimate-multisite'); | ||
|
|
||
| // translators: %s is the paypal error message. | ||
| return '<p>' . sprintf(__('An unexpected PayPal error occurred. Error message: %s.', 'ultimate-multisite'), $error) . '</p>'; | ||
| $error_message = sprintf(__('An unexpected PayPal error occurred. Error message: %s.', 'ultimate-multisite'), esc_html($error)); | ||
|
|
||
| wp_die( | ||
| esc_html($error_message), | ||
| esc_html__('PayPal Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| /* | ||
| * Validate that the pending payment exists. | ||
| */ | ||
| $pending_payment = $checkout_details['pending_payment'] ?? null; | ||
|
|
||
| if (empty($pending_payment)) { | ||
| wu_log_add('paypal', 'PayPal confirmation failed: pending payment not found', LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html__('Unable to locate the pending payment. Please try again or contact support.', 'ultimate-multisite'), | ||
| esc_html__('Payment Not Found', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| /* | ||
| * Compiles the necessary elements. | ||
| */ | ||
| $customer = $checkout_details['pending_payment']->get_customer(); // current customer | ||
| $customer = $pending_payment->get_customer(); | ||
| $membership = $pending_payment->get_membership(); | ||
|
|
||
| if (empty($customer) || empty($membership)) { | ||
| wu_log_add('paypal', 'PayPal confirmation failed: customer or membership not found', LogLevel::ERROR); | ||
|
|
||
| wp_die( | ||
| esc_html__('Unable to locate customer or membership data. Please try again or contact support.', 'ultimate-multisite'), | ||
| esc_html__('Data Error', 'ultimate-multisite'), | ||
| [ | ||
| 'back_link' => true, | ||
| 'response' => '200', | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| wu_get_template( | ||
| 'checkout/paypal/confirm', | ||
| [ | ||
| 'checkout_details' => $checkout_details, | ||
| 'customer' => $customer, | ||
| 'payment' => $checkout_details['pending_payment'], | ||
| 'membership' => $checkout_details['pending_payment']->get_membership(), | ||
| 'payment' => $pending_payment, | ||
| 'membership' => $membership, | ||
| ] | ||
| ); | ||
| } | ||
|
|
@@ -1615,12 +1768,27 @@ | |
| $message = wp_remote_retrieve_response_message($request); | ||
|
|
||
| if (is_wp_error($request)) { | ||
| wu_log_add('paypal', 'GetExpressCheckoutDetails request failed: ' . $request->get_error_message(), LogLevel::ERROR); | ||
|
|
||
| return $request; | ||
| } elseif (200 === absint($code) && 'OK' === $message) { | ||
| if (is_string($body)) { | ||
| wp_parse_str($body, $body); | ||
| } | ||
|
|
||
| /* | ||
| * Check for PayPal API errors. | ||
| * ACK=Failure means the API call failed (e.g., invalid credentials, expired token). | ||
| */ | ||
| if (isset($body['ACK']) && ('failure' === strtolower((string) $body['ACK']) || 'failurewithwarning' === strtolower((string) $body['ACK']))) { | ||
| $error_code = $body['L_ERRORCODE0'] ?? 'unknown'; | ||
| $error_message = $body['L_LONGMESSAGE0'] ?? __('Unknown PayPal error', 'ultimate-multisite'); | ||
|
|
||
| wu_log_add('paypal', sprintf('GetExpressCheckoutDetails failed with error %s: %s', $error_code, $error_message), LogLevel::ERROR); | ||
|
|
||
| return new \WP_Error($error_code, $error_message); | ||
| } | ||
|
|
||
| $payment_id = absint(wu_request('payment-id')); | ||
|
|
||
| $pending_payment = $payment_id ? wu_get_payment($payment_id) : wu_get_payment_by_hash(wu_request('payment')); | ||
|
|
@@ -1631,11 +1799,13 @@ | |
|
|
||
| $body['pending_payment'] = $pending_payment; | ||
|
|
||
| $custom = explode('|', (string) $body['PAYMENTREQUEST_0_CUSTOM']); | ||
| $custom = explode('|', (string) wu_get_isset($body, 'PAYMENTREQUEST_0_CUSTOM', '')); | ||
|
|
||
| return $body; | ||
| } | ||
|
|
||
| wu_log_add('paypal', sprintf('GetExpressCheckoutDetails received unexpected response: HTTP %d %s', $code, $message), LogLevel::ERROR); | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused variable.
The
$customvariable is assigned but never used. This appears to be dead code.🔎 Proposed fix
Based on static analysis hint from PHPMD.
📝 Committable suggestion
🧰 Tools
🪛 PHPMD (2.15.0)
1802-1802: Avoid unused local variables such as '$custom'. (undefined)
(UnusedLocalVariable)
🤖 Prompt for AI Agents