Skip to content

Conversation

@superdav42
Copy link
Collaborator

@superdav42 superdav42 commented Dec 23, 2025

Summary

Fixes the issue where PayPal checkout would silently loop back to the /register page instead of showing an error message when PayPal API calls fail (e.g., error 10002 "Security header is not valid" due to invalid credentials).

Root Causes Fixed

  1. get_checkout_details() not checking ACK=Failure - The function only checked HTTP status code (200 OK) but not the PayPal ACK field. When API credentials were invalid, PayPal would return HTTP 200 with ACK=Failure, which was being treated as valid data.

  2. confirmation_form() returned errors instead of displaying them - When an error occurred, the function returned an error string, but the calling code used output buffering and ignored return values, so errors were never shown to users.

  3. Missing validation for required data - No validation for pending_payment, customer, or membership objects before trying to render the confirmation form.

Changes

  • get_checkout_details(): Added ACK=Failure check to detect PayPal API errors, returns WP_Error on failures instead of partial data, added comprehensive logging
  • confirmation_form(): Changed return type from string to void, uses wp_die() to display errors, added validation for required objects
  • process_confirmation(): Added proper WP_Error handling, improved error messages with better context, added logging for all error conditions
  • create_recurring_profile() & complete_single_payment(): Added logging for API call lifecycle, added ACK=FailureWithWarning check, improved error messages to include PayPal error code and message

Result

Users now see clear error messages instead of being silently redirected back to the checkout page. All PayPal API errors are logged for debugging.

Test plan

  • Test PayPal checkout with valid credentials - should complete successfully
  • Test PayPal checkout with invalid credentials - should show error message instead of looping
  • Test expired PayPal token scenario - should show clear error message
  • Check PayPal logs (/wp-admin/network/admin.php?page=wp-ultimo-logs&tab=paypal) for proper logging
  • Test recurring payment profile creation
  • Test single payment completion

Fixes #193

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Enhanced PayPal payment error handling with clearer, user-friendly error messages for various failure scenarios
    • Improved detection and handling of invalid or expired payment tokens
    • Added comprehensive logging throughout payment confirmation and processing flows for better troubleshooting
    • Standardized error responses and recovery options across payment failure paths

✏️ Tip: You can customize this high-level summary in your review settings.

## Problem
When PayPal returns an error (e.g., error 10002 "Security header is not valid"
due to invalid API credentials), the checkout would silently loop back to the
/register page instead of showing an error message to the user.

## Root Causes Fixed

1. **get_checkout_details() not checking ACK=Failure**: The function only checked
   HTTP status code (200 OK) but not the PayPal ACK field. When API credentials
   were invalid, PayPal would return HTTP 200 with ACK=Failure, which was being
   treated as a valid response.

2. **confirmation_form() returned errors instead of displaying them**: When an
   error occurred, the function returned an error string but the calling code
   used output buffering and ignored return values, so errors were never shown.

3. **Missing validation for required data**: No validation for pending_payment,
   customer, or membership objects before trying to render the confirmation form.

## Changes

### get_checkout_details()
- Added ACK=Failure check to detect PayPal API errors
- Returns WP_Error on API failures instead of partial data
- Added comprehensive logging for all error conditions

### confirmation_form()
- Changed return type from string to void
- Uses wp_die() to display errors instead of returning strings
- Added validation for pending_payment, customer, and membership
- Added error logging

### process_confirmation()
- Added proper WP_Error handling for get_checkout_details() response
- Improved all error messages with better context
- Added logging for all error conditions
- Uses proper wp_die() calls with back_link option

### create_recurring_profile() & complete_single_payment()
- Added logging for API call start, success, and failure
- Added ACK=FailureWithWarning check (not just Failure)
- Improved error messages to include PayPal error code and message
- Fixed HTTP response code from 401 to 200 for proper back links

## Result
Users now see clear error messages instead of being silently redirected.
All PayPal API errors are logged for debugging.

Fixes #193

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 23, 2025

Walkthrough

The PR enhances the PayPal gateway error handling and logging throughout the payment confirmation flow. It adds comprehensive error detection for API failures, improves user-facing error messages, integrates debugging logs across confirmation and payment processing, updates the confirmation_form return type to void, and standardizes error responses with consistent 200 HTTP status codes and back links.

Changes

Cohort / File(s) Summary
Confirmation flow error handling
inc/gateways/class-paypal-gateway.php
Enhanced process_confirmation() with detailed error detection for WP_Error from checkout details retrieval, invalid/empty tokens, missing pending payments, and absent membership/customer records. Each path now includes logging and user-friendly error messages with standardized 200-like responses and back links.
Checkout details and form validation
inc/gateways/class-paypal-gateway.php
Updated get_checkout_details() to log API failures and return WP_Error on ACK failures or false on unexpected responses. Refactored confirmation_form() to return void, replacing direct checkout_details access with explicit pending_payment and membership retrieval, adding error handling with logging and wp_die calls for missing records.
Recurring profile creation
inc/gateways/class-paypal-gateway.php
Added logging before CreateRecurringPaymentsProfile request in create_recurring_profile(). Enhanced error handling for WP_Error and PayPal responses (treating failure and failurewithwarning equivalently), with logging of errors/success details and standardized die blocks on failure.
Single payment processing
inc/gateways/class-paypal-gateway.php
Added logging entry and enhanced complete_single_payment() to handle DoExpressCheckoutPayment failures and unexpected HTTP responses with logged errors and user-friendly error messages. Success paths now log transaction details for tracing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: fixing a PayPal checkout loop bug and improving error handling throughout the gateway implementation.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #193 by implementing ACK validation, proper error handling with WP_Error, logging of PayPal API failures, and user-facing error messages via wp_die() to prevent silent checkout loops.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the PayPal checkout loop and error handling issues identified in #193; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/paypal-checkout-loop-193

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
inc/gateways/class-paypal-gateway.php (1)

848-852: Consider using integer for HTTP response code.

The response parameter in wp_die() expects an integer. While the string '200' works (WordPress casts it), using an integer 200 would be more conventional and explicit.

This pattern appears in multiple wp_die() calls throughout the file (lines 848, 862, 885, 906, 927, 1351, 1381, 1477, 1558, 1580, 1658, 1687, 1705, 1724).

🔎 Example fix (apply to all occurrences)
-					'response'  => '200',
+					'response'  => 200,
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 068753e and 134ca8b.

📒 Files selected for processing (1)
  • inc/gateways/class-paypal-gateway.php
🧰 Additional context used
🪛 PHPMD (2.15.0)
inc/gateways/class-paypal-gateway.php

1802-1802: Avoid unused local variables such as '$custom'. (undefined)

(UnusedLocalVariable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: cypress (8.2, chrome)
  • GitHub Check: cypress (8.1, chrome)
  • GitHub Check: PHP 8.4
  • GitHub Check: Build Plugin for Testing
  • GitHub Check: PHP 8.1
🔇 Additional comments (7)
inc/gateways/class-paypal-gateway.php (7)

833-866: LGTM!

Good addition of WP_Error handling. The error detection for both WP_Error objects and empty/invalid responses covers the main failure scenarios. Logging with LogLevel::ERROR and using wp_die() with back_link provides clear user feedback and debugging capability.


878-931: LGTM!

Consistent error handling for missing required objects (payment, cart, membership, customer). Each failure path now logs the error and provides a user-friendly message with a back link, addressing the original issue where errors were silently swallowed.


1333-1397: LGTM!

Comprehensive error handling for CreateRecurringPaymentsProfile. The changes properly:

  • Log before initiating the request
  • Handle WP_Error from wp_remote_post
  • Check for both failure and failurewithwarning ACK values
  • Extract and display PayPal error codes/messages
  • Log success with transaction details for debugging

1469-1480: LGTM!

Good addition of logging for unexpected HTTP responses before the wp_die() call. This will help diagnose edge cases where PayPal returns non-200 status codes.


1532-1662: LGTM!

The complete_single_payment method now has consistent error handling that mirrors create_recurring_profile. The addition of logging at entry, for failures, and on success provides good observability for debugging payment issues.


1668-1738: LGTM!

The confirmation_form method now properly validates all required objects before rendering the template:

  1. Validates checkout details response
  2. Checks for pending payment existence
  3. Verifies customer and membership data

The return type change to void is appropriate since the method now uses wp_die() for errors instead of returning error strings.


1779-1790: LGTM - Core fix for the checkout loop issue.

This is the critical fix addressing the root cause from Issue #193. The method now properly checks for ACK=Failure and ACK=FailureWithWarning responses and returns a WP_Error instead of treating the response as valid. This prevents the silent loop back to /register.

$body['pending_payment'] = $pending_payment;

$custom = explode('|', (string) $body['PAYMENTREQUEST_0_CUSTOM']);
$custom = explode('|', (string) wu_get_isset($body, 'PAYMENTREQUEST_0_CUSTOM', ''));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused variable.

The $custom variable is assigned but never used. This appears to be dead code.

🔎 Proposed fix
-			$custom = explode('|', (string) wu_get_isset($body, 'PAYMENTREQUEST_0_CUSTOM', ''));
-
 			return $body;

Based on static analysis hint from PHPMD.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$custom = explode('|', (string) wu_get_isset($body, 'PAYMENTREQUEST_0_CUSTOM', ''));
return $body;
🧰 Tools
🪛 PHPMD (2.15.0)

1802-1802: Avoid unused local variables such as '$custom'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In inc/gateways/class-paypal-gateway.php around line 1802, the variable $custom
is assigned from explode(...) but never used; remove that unused assignment line
to eliminate dead code and satisfy PHPMD static analysis.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PayPal checkout loops back to /register (sandbox). NVP capture fails with error 10002.

2 participants