Skip to content

Conversation

@superdav42
Copy link
Collaborator

@superdav42 superdav42 commented Dec 15, 2025

Overview

This PR integrates a secure proxy server architecture for Stripe Connect OAuth flow, eliminating the need to expose platform credentials in the distributed Ultimate Multisite plugin code.

Security Improvements

No credentials in distributed code - Platform Client ID and Secret Keys never leave the proxy server at ultimatemultisite.com
Encrypted communication - OAuth codes encrypted (AES-256-CBC) during transmission
Centralized control - Rotate credentials without updating plugin
Usage tracking - Proxy database tracks all connected sites

Changes

Base Stripe Gateway (inc/gateways/class-base-stripe-gateway.php)

New Methods:

  • get_proxy_url() - Returns proxy server URL (filterable via wu_stripe_connect_proxy_url)
  • get_business_data() - Provides site info for Stripe Connect form prefill

Updated Methods:

  • get_connect_authorization_url() - Now calls proxy /oauth/init endpoint
  • handle_oauth_callbacks() - Handles encrypted codes from proxy (wcs_stripe_code, wcs_stripe_state parameters)
  • exchange_code_for_keys() - Calls proxy /oauth/keys endpoint (replaces process_oauth_callback())
  • handle_disconnect() - Notifies proxy server when disconnecting

Removed Methods:

  • get_platform_client_id() - Credentials now on proxy
  • get_platform_secret_key() - Credentials now on proxy

Security Fixes:

  • Sanitize all $_GET input variables

Stripe Gateway (inc/gateways/class-stripe-gateway.php)

  • Remove local client ID check (proxy handles this)
  • Code style improvements

Tests

  • Updated test_oauth_authorization_url_generation() to mock proxy response
  • Updated test_oauth_authorization_url_requires_client_id() to test proxy failure
  • All OAuth tests passing: 19 tests, 53 assertions

Architecture Flow

1. User clicks "Connect with Stripe"
   ↓
2. Plugin calls https://ultimatemultisite.com/wp-json/stripe-connect/v1/oauth/init
   ↓
3. Proxy returns OAuth URL with encrypted state
   ↓
4. User authorizes on Stripe
   ↓
5. Stripe redirects to proxy /oauth/redirect endpoint
   ↓
6. Proxy encrypts code and redirects back to site
   ↓
7. Plugin calls proxy /oauth/keys to exchange encrypted code
   ↓
8. Proxy decrypts code, exchanges with Stripe, returns tokens
   ↓
9. Plugin saves tokens locally

Proxy Server

The proxy server is now available at: https://github.com/superdav42/stripe-connect-proxy

Features:

  • REST API endpoints for OAuth flow
  • AES-256-CBC encryption for secure code transmission
  • Database tracking of connected sites
  • Complete documentation and integration guides

Deployment Requirements

⚠️ Before merging: The proxy server plugin must be:

  1. Deployed to ultimatemultisite.com
  2. Configured with platform credentials in wp-config.php
  3. Activated
  4. Stripe redirect URL updated to: https://ultimatemultisite.com/wp-json/stripe-connect/v1/oauth/redirect

See proxy repository INTEGRATION.md for detailed deployment instructions.

Backwards Compatibility

✅ Fully backwards compatible with existing direct API key mode
✅ Sites with existing OAuth connections continue to work
✅ New OAuth connections automatically use proxy server

Testing

# Run OAuth tests
vendor/bin/phpunit --filter Stripe_OAuth --testdox

All tests passing with no regressions.


🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Full migration to Stripe Payment Element for checkout, improved initialization, styling, validation, and error feedback
    • Stripe Connect (OAuth) support with connect/disconnect flows and admin UI status
    • Payment status polling and scheduled fallback verification for Stripe payments
  • Tests

    • New PHPUnit suites covering OAuth flows, token/account persistence, and end-to-end scenarios
  • Bug Fixes

    • Improved payment verification and resubmission handling to reduce pending/failed states

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

Replace direct OAuth implementation with secure proxy server architecture
to keep platform credentials safe and never expose them in distributed code.

## Security Improvements:

✅ **No credentials in distributed code** - Platform Client ID and Secret Keys
   never leave the proxy server at ultimatemultisite.com
✅ **Encrypted communication** - OAuth codes encrypted during transmission
✅ **Centralized control** - Rotate credentials without updating plugin
✅ **Usage tracking** - Proxy database tracks all connected sites

## Changes to Base Stripe Gateway (inc/gateways/class-base-stripe-gateway.php):

### New Methods:
- `get_proxy_url()` - Returns proxy server URL (filterable)
- `get_business_data()` - Provides site info for Stripe form prefill

### Updated Methods:
- `get_connect_authorization_url()` - Now calls proxy /oauth/init endpoint
  instead of directly constructing Stripe OAuth URL
- `handle_oauth_callbacks()` - Handles encrypted codes from proxy
  (looks for wcs_stripe_code and wcs_stripe_state parameters)
- `exchange_code_for_keys()` - Calls proxy /oauth/keys endpoint to exchange
  encrypted code for API keys (replaces process_oauth_callback)
- `handle_disconnect()` - Notifies proxy server when disconnecting

### Removed Methods:
- `get_platform_client_id()` - No longer needed (credentials on proxy)
- `get_platform_secret_key()` - No longer needed (credentials on proxy)

### Security Fixes:
- Sanitize all $_GET input variables (wcs_stripe_code, _wpnonce)

## Changes to Stripe Gateway (inc/gateways/class-stripe-gateway.php):

- Remove client ID check (proxy handles this now)
- Auto-fixed code style issues

## Test Updates:

- Updated `test_oauth_authorization_url_generation()` to mock proxy response
- Updated `test_oauth_authorization_url_requires_client_id()` to test proxy failure
- All tests passing: 19 tests, 53 assertions ✅

## Architecture Flow:

1. User clicks "Connect with Stripe"
2. Plugin calls https://ultimatemultisite.com/wp-json/stripe-connect/v1/oauth/init
3. Proxy returns OAuth URL with encrypted state
4. User authorizes on Stripe
5. Stripe redirects to proxy /oauth/redirect endpoint
6. Proxy encrypts code and redirects back to site
7. Plugin calls proxy /oauth/keys to exchange encrypted code
8. Proxy decrypts code, exchanges with Stripe, returns tokens
9. Plugin saves tokens locally

## Proxy Server Repository:
https://github.com/superdav42/stripe-connect-proxy

## Deployment Notes:

The proxy server plugin must be deployed to ultimatemultisite.com and
configured with platform credentials before OAuth connections will work.
See INTEGRATION.md in the proxy repository for deployment instructions.

## Backwards Compatibility:

Fully backwards compatible with existing direct API key mode. Sites that
have already connected via OAuth will continue to work. New OAuth connections
will automatically use the proxy server.

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

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

coderabbitai bot commented Dec 15, 2025

📝 Walkthrough

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Use Stripe Checkout Element' refers to a real aspect of the changes (Payment Element replaces Card Element in the frontend JS), but the PR's primary objective is implementing Stripe Connect OAuth integration via a proxy architecture, which is the dominant theme across the PHP files and tests. Consider updating the title to reflect the main objective, e.g., 'Add Stripe Connect OAuth integration with proxy architecture' or 'Implement Stripe OAuth with centralized proxy handling'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

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: 4

🧹 Nitpick comments (3)
assets/js/gateways/stripe.js (1)

66-88: Payment Element initialization is correct, but style update is ineffective.

The Payment Element is correctly created with the clientSecret and mounted. However, the call to wu_stripe_update_payment_element_styles at line 83 will not work as noted above. Consider integrating the dynamic styles into the initial appearance object at lines 73-75.

inc/gateways/class-base-stripe-gateway.php (1)

467-482: Consider using admin notices instead of wp_die for OAuth errors.

Using wp_die() provides a poor user experience when OAuth fails. Consider redirecting back to settings with an error notice instead.

 if (is_wp_error($response)) {
-    wp_die(esc_html__('Failed to connect to proxy server', 'ultimate-multisite'));
+    $redirect_url = add_query_arg(
+        [
+            'page'               => 'wu-settings',
+            'tab'                => 'payment-gateways',
+            'stripe_oauth_error' => 'proxy_connection_failed',
+        ],
+        admin_url('admin.php')
+    );
+    wp_safe_redirect($redirect_url);
+    exit;
 }
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (1)

61-85: Consider cleaning up filters after tests.

Filters added in tests persist and may affect subsequent tests. Consider adding a tearDown method to remove filters or storing the filter callback to remove it after assertions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8490fd0 and 15568bf.

📒 Files selected for processing (5)
  • assets/js/gateways/stripe.js (4 hunks)
  • inc/gateways/class-base-stripe-gateway.php (6 hunks)
  • inc/gateways/class-stripe-gateway.php (8 hunks)
  • tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (1 hunks)
  • tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
assets/js/gateways/stripe.js (1)
assets/js/checkout.js (1)
  • promises (864-864)
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (2)
inc/functions/settings.php (1)
  • wu_save_setting (51-54)
inc/gateways/class-base-stripe-gateway.php (3)
  • init (194-211)
  • is_using_oauth (292-294)
  • get_authentication_mode (282-284)
tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php (2)
inc/functions/settings.php (2)
  • wu_save_setting (51-54)
  • wu_get_setting (37-40)
inc/gateways/class-base-stripe-gateway.php (4)
  • init (194-211)
  • get_authentication_mode (282-284)
  • is_using_oauth (292-294)
  • get_connect_authorization_url (356-392)
inc/gateways/class-stripe-gateway.php (2)
inc/functions/settings.php (1)
  • wu_register_settings_field (88-91)
inc/gateways/class-base-stripe-gateway.php (3)
  • is_using_oauth (292-294)
  • get_disconnect_url (400-410)
  • get_connect_authorization_url (356-392)
🪛 Biome (2.1.2)
assets/js/gateways/stripe.js

[error] 27-45: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)


[error] 302-302: Invalid typeof comparison.

Compare with one of the following string literals:

(lint/correctness/useValidTypeof)

🪛 GitHub Check: Code Quality Checks
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php

[warning] 168-168:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space


[failure] 146-146:
Only one argument is allowed per line in a multi-line function call


[failure] 146-146:
Opening parenthesis of a multi-line function call must be the last content on the line


[failure] 145-145:
Closing parenthesis of a multi-line function call must be on a line by itself


[failure] 143-143:
Expected 1 space after FUNCTION keyword; 0 found


[failure] 143-143:
Only one argument is allowed per line in a multi-line function call


[failure] 143-143:
Opening parenthesis of a multi-line function call must be the last content on the line


[warning] 128-128:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space


[warning] 105-105:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space


[warning] 81-81:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space


[failure] 65-65:
Closing parenthesis of a multi-line function call must be on a line by itself


[failure] 63-63:
Expected 1 space after FUNCTION keyword; 0 found


[failure] 63-63:
Only one argument is allowed per line in a multi-line function call


[failure] 63-63:
Opening parenthesis of a multi-line function call must be the last content on the line


[warning] 53-53:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php

[warning] 157-157:
Array double arrow not aligned correctly; expected 4 space(s) between "'state'" and double arrow, but found 1.


[warning] 155-155:
Array double arrow not aligned correctly; expected 5 space(s) between "'body'" and double arrow, but found 1.


[warning] 140-140:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space


[warning] 118-118:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

🪛 Gitleaks (8.30.0)
tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php

[high] 66-66: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 85-85: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 128-128: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 198-198: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 243-243: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 303-303: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 PHPMD (2.15.0)
inc/gateways/class-base-stripe-gateway.php

356-356: Avoid unused parameters such as '$state'. (undefined)

(UnusedFormalParameter)

tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php

151-151: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)


180-180: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)

⏰ 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). (4)
  • GitHub Check: PHP 8.3
  • GitHub Check: cypress (8.2, chrome)
  • GitHub Check: Build Plugin for Testing
  • GitHub Check: cypress (8.1, chrome)
🔇 Additional comments (17)
assets/js/gateways/stripe.js (1)

374-413: LGTM - Payment Element confirmation flow.

The handlePaymentElement function correctly uses confirmPayment with redirect: 'if_required' and properly handles both success (resubmit) and error (display) cases.

inc/gateways/class-stripe-gateway.php (4)

48-49: LGTM - OAuth callback hook registration.

The admin_init hook properly delegates to the parent class's handle_oauth_callbacks method for OAuth flow processing.


893-910: State regeneration on every render.

A new OAuth state is generated each time this method is called (e.g., on page refresh). While this is secure, it means that if a user opens the settings page in multiple tabs, only the last tab's OAuth flow will work. This is generally acceptable behavior for OAuth CSRF protection.


745-753: LGTM - Dual element containers for Payment Element migration.

The addition of #payment-element alongside the existing #card-element enables gradual migration to the modern Payment Element while maintaining backwards compatibility.


134-150: LGTM - Direct API keys toggle.

The new stripe_show_direct_keys toggle allows advanced users to enter manual API keys while keeping OAuth as the recommended default.

inc/gateways/class-base-stripe-gateway.php (7)

84-131: LGTM - OAuth state properties.

The new protected properties properly encapsulate OAuth-related state including tokens, account ID, and application fee. Default values are appropriately set.


148-163: LGTM - Stripe Connect account header configuration.

The stripe_account header is correctly set when using OAuth mode, enabling API calls on behalf of the connected account.


223-274: LGTM - OAuth precedence in key setup.

The setup_api_keys method correctly prioritizes OAuth tokens over direct API keys, with proper fallback behavior for backwards compatibility.


418-438: LGTM - OAuth callback security.

The handler properly verifies the CSRF state before processing OAuth callbacks and uses nonce verification for disconnect requests. All $_GET inputs are properly sanitized.


349-392: Unused $state parameter is intentional.

The static analysis correctly identifies that $state is unused. The docblock at line 353 correctly documents this as kept for compatibility since the proxy server now generates and manages the state. No change needed.


1493-1498: LGTM - Application fee integration.

The application fee is correctly applied only when using OAuth mode with a non-zero fee percentage, using Stripe's application_fee_percent parameter.


522-565: LGTM - Disconnect handling clears all tokens.

The disconnect handler correctly clears OAuth tokens for both test and live modes, ensuring complete cleanup regardless of the current sandbox mode setting. The non-blocking proxy notification is appropriate.

tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (1)

138-189: LGTM - Comprehensive E2E OAuth flow test.

The test thoroughly validates the complete OAuth lifecycle including initial setup, token storage, application fee loading, and fallback to direct keys when OAuth is disconnected.

tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php (4)

151-162: Unused $args parameter is expected for WordPress filter signature.

The pre_http_request filter requires the signature ($preempt, $args, $url). The unused $args parameter is intentional and cannot be removed. Consider adding a PHPDoc annotation to suppress the warning.

-        add_filter('pre_http_request', function($preempt, $args, $url) {
+        // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Required by filter signature
+        add_filter('pre_http_request', function($preempt, $args, $url) {

64-75: Gitleaks false positives for test API keys.

The flagged API keys (pk_test_direct_123, sk_test_direct_123, etc.) are intentionally fake test fixtures, not real credentials. These follow Stripe's test key naming convention and pose no security risk.


17-42: LGTM - Well-structured test setup.

The test class properly initializes a clean state before each test by clearing all Stripe settings. The use of wu_save_setting with empty values ensures test isolation.


334-347: LGTM - Disconnect URL security test.

This test properly verifies that the disconnect URL includes the required nonce for CSRF protection and all necessary query parameters.

Comment on lines 63 to 65
add_filter('wu_stripe_application_fee_percentage', function() {
return 3.5;
});
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

Fix code style for multi-line closure.

The CI pipeline is failing due to code style issues. The closure needs proper formatting per the project's coding standards.

-        add_filter('wu_stripe_application_fee_percentage', function() {
-            return 3.5;
-        });
+        add_filter(
+            'wu_stripe_application_fee_percentage',
+            function () {
+                return 3.5;
+            }
+        );
📝 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
add_filter('wu_stripe_application_fee_percentage', function() {
return 3.5;
});
add_filter(
'wu_stripe_application_fee_percentage',
function () {
return 3.5;
}
);
🧰 Tools
🪛 GitHub Check: Code Quality Checks

[failure] 65-65:
Closing parenthesis of a multi-line function call must be on a line by itself


[failure] 63-63:
Expected 1 space after FUNCTION keyword; 0 found


[failure] 63-63:
Only one argument is allowed per line in a multi-line function call


[failure] 63-63:
Opening parenthesis of a multi-line function call must be the last content on the line

🤖 Prompt for AI Agents
In tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php around lines 63-65, the
anonymous function passed to add_filter is not formatted according to project
style; reformat the multi-line closure so the function keyword has a space
before the parentheses (function ()), place the opening brace on the same line,
put the return statement on its own indented line, and place the closing brace
and ); on their own lines to match the project's PHP coding standards.

@superdav42 superdav42 changed the title Integrate Stripe Connect Proxy Server for Secure OAuth Use Stripe Checkout Element Dec 15, 2025
Performance Improvements:
- Implement lazy loading for OAuth initialization
- Move proxy HTTP request from page load to button click
- Add get_oauth_init_url() method for deferred OAuth flow
- Update handle_oauth_callbacks() to process init requests

Application Fee Removal:
- Remove application_fee_percentage property and methods
- Remove fee application from subscription creation
- Clean up OAuth tests to remove fee-related assertions
- Remove 4 application fee tests from test suite

Technical Details:
- OAuth URL generation now happens only when user clicks "Connect with Stripe"
- State generation moved to init handler for better security
- All 15 OAuth tests passing (44 assertions)

This eliminates unnecessary HTTP requests on every settings page load
and simplifies the codebase by removing unused application fee logic.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@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

♻️ Duplicate comments (1)
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (1)

90-92: Fix closure formatting to pass CI checks.

The multi-line closure violates project coding standards. Apply proper formatting.

-		add_filter('wu_stripe_platform_client_id', function() {
-			return 'ca_platform_test_123';
-		});
+		add_filter(
+			'wu_stripe_platform_client_id',
+			function () {
+				return 'ca_platform_test_123';
+			}
+		);
🧹 Nitpick comments (2)
inc/gateways/class-base-stripe-gateway.php (2)

318-361: Remove unused $state parameter.

The $state parameter is never used in the method body since state is now managed by the proxy server. Consider removing it or documenting why it's kept for interface compatibility.

-	public function get_connect_authorization_url(string $state = ''): string {
+	public function get_connect_authorization_url(): string {

Based on static analysis hint about unused parameter.


422-433: Consider adding error logging for failed state verification.

When state verification fails, the code silently ignores the callback. Adding a log entry would help diagnose OAuth issues in production.

 		// Verify CSRF state
 		$expected_state = get_option('wu_stripe_oauth_state');
 
 		if ($expected_state && $expected_state === $state) {
 			$this->exchange_code_for_keys($encrypted_code);
+		} else {
+			wu_log_add('stripe', 'OAuth callback state verification failed', \Psr\Log\LogLevel::WARNING);
 		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15568bf and bd87731.

📒 Files selected for processing (4)
  • inc/gateways/class-base-stripe-gateway.php (5 hunks)
  • inc/gateways/class-stripe-gateway.php (8 hunks)
  • tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (1 hunks)
  • tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
inc/gateways/class-stripe-gateway.php (2)
inc/functions/settings.php (1)
  • wu_register_settings_field (88-91)
inc/gateways/class-base-stripe-gateway.php (3)
  • is_using_oauth (279-281)
  • get_disconnect_url (390-400)
  • get_oauth_init_url (372-382)
inc/gateways/class-base-stripe-gateway.php (2)
inc/functions/settings.php (2)
  • wu_get_setting (37-40)
  • wu_save_setting (51-54)
inc/functions/string-helpers.php (1)
  • wu_replace_dashes (50-53)
tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php (4)
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (1)
  • setUp (27-30)
inc/functions/settings.php (2)
  • wu_save_setting (51-54)
  • wu_get_setting (37-40)
inc/gateways/class-stripe-gateway.php (1)
  • Stripe_Gateway (28-909)
inc/gateways/class-base-stripe-gateway.php (4)
  • init (186-203)
  • get_authentication_mode (269-271)
  • is_using_oauth (279-281)
  • get_connect_authorization_url (325-361)
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (2)
inc/functions/settings.php (1)
  • wu_save_setting (51-54)
inc/gateways/class-base-stripe-gateway.php (3)
  • init (186-203)
  • is_using_oauth (279-281)
  • get_authentication_mode (269-271)
🪛 GitHub Check: Code Quality Checks
tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php

[warning] 183-183:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space


[warning] 160-160:
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space


[warning] 138-138:
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space


[warning] 115-115:
Equals sign not aligned with surrounding assignments; expected 5 spaces but found 1 space


[warning] 107-107:
Array double arrow not aligned correctly; expected 4 space(s) between "'state'" and double arrow, but found 1.


[failure] 105-105:
Opening parenthesis of a multi-line function call must be the last content on the line


[warning] 105-105:
Array double arrow not aligned correctly; expected 5 space(s) between "'body'" and double arrow, but found 1.


[failure] 101-101:
Expected 1 space after FUNCTION keyword; 0 found


[failure] 101-101:
Only one argument is allowed per line in a multi-line function call


[failure] 101-101:
Opening parenthesis of a multi-line function call must be the last content on the line


[failure] 1-1:
Class file names should be based on the class name with "class-" prepended. Expected class-stripe-oauth-test.php, but found Stripe_OAuth_Test.php.


[failure] 1-1:
Filenames should be all lowercase with hyphens as word separators. Expected stripe-oauth-test.php, but found Stripe_OAuth_Test.php.

tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php

[warning] 112-112:
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space


[failure] 92-92:
Closing parenthesis of a multi-line function call must be on a line by itself


[failure] 90-90:
Expected 1 space after FUNCTION keyword; 0 found


[failure] 90-90:
Only one argument is allowed per line in a multi-line function call


[failure] 90-90:
Opening parenthesis of a multi-line function call must be the last content on the line


[warning] 75-75:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space


[warning] 53-53:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

🪛 Gitleaks (8.30.0)
tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php

[high] 65-65: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 84-84: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 148-148: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 193-193: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 253-253: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 PHPMD (2.15.0)
inc/gateways/class-base-stripe-gateway.php

325-325: Avoid unused parameters such as '$state'. (undefined)

(UnusedFormalParameter)

tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php

101-101: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)


130-130: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)

⏰ 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: PHP 8.2
  • GitHub Check: PHP 7.4
  • GitHub Check: Build Plugin for Testing
  • GitHub Check: cypress (8.1, chrome)
  • GitHub Check: cypress (8.2, chrome)
🔇 Additional comments (20)
inc/gateways/class-stripe-gateway.php (5)

48-49: LGTM - OAuth callback hook properly wired.

The OAuth callback handler is correctly hooked to admin_init, which ensures OAuth redirects and disconnects are processed in the admin context before any output is sent.


104-150: OAuth settings fields are well-structured.

The new settings fields for Stripe authentication are properly registered with appropriate visibility controls via require arrays. The toggle for direct API keys provides good backwards compatibility.


183-186: Direct key fields now correctly gated behind toggle.

The stripe_show_direct_keys requirement ensures API key fields only appear when the user explicitly opts for manual key entry, keeping the OAuth flow as the default recommended path.

Also applies to: 203-207, 224-228, 245-249


745-753: Payment Element container added for modern Stripe integration.

The new payment-element div provides the mount point for Stripe's modern Payment Element, while keeping the legacy card-element container for fallback support.


860-908: OAuth connection HTML method is well-implemented.

The get_oauth_connection_html() method properly escapes all output using esc_html(), esc_url(), and includes i18n-ready strings. The connected/disconnected states are clearly distinguished with appropriate visual styling.

inc/gateways/class-base-stripe-gateway.php (6)

84-122: OAuth properties well-defined with sensible defaults.

The new OAuth-related properties (is_connect_enabled, oauth_access_token, oauth_account_id, platform_client_id, authentication_mode) are properly typed and initialized with secure defaults.


140-155: Stripe client correctly configured for Connected Accounts.

The stripe_account header is properly injected when using OAuth mode, which ensures API calls are directed to the connected merchant's account rather than the platform account.


215-261: Dual authentication logic is correct and well-documented.

The setup_api_keys() method properly prioritizes OAuth tokens over direct keys, setting appropriate flags for each mode. The fallback behavior ensures backwards compatibility.


408-441: OAuth callback handling has proper security controls.

The handler correctly verifies nonces for init and disconnect actions, and validates the CSRF state for OAuth callbacks. The page context check ('wu-settings' === $_GET['page']) adds an additional layer of protection.


450-517: Token exchange implementation is secure and handles errors appropriately.

The method properly validates the response status code and required fields before saving tokens. Using wp_die() for critical errors is acceptable for an admin-initiated flow.


525-568: Disconnect handler properly clears all OAuth tokens.

The method correctly clears tokens for both test and live modes, and uses non-blocking request to notify the proxy. This ensures a clean disconnect regardless of proxy availability.

tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (3)

27-30: LGTM - Test setup correctly resets state.

The setUp() method properly calls the helper to clear all Stripe settings before each test, ensuring test isolation.


35-56: Good test coverage for OAuth token persistence.

The test verifies that saved OAuth tokens are correctly loaded by the gateway and that OAuth mode is properly detected.


85-128: Comprehensive OAuth flow simulation test.

This test effectively validates the complete OAuth lifecycle including initial setup, token persistence, mode detection, and fallback to direct keys after disconnect.

tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php (6)

29-41: Good test setup with proper state isolation.

The setUp() method correctly clears relevant Stripe settings and initializes a fresh gateway instance for each test.


46-58: Solid coverage for authentication mode detection.

The tests properly verify OAuth mode detection, direct mode detection, and that OAuth takes precedence when both are configured.

Also applies to: 63-74, 79-94


65-66: Gitleaks false positive - these are intentionally fake test keys.

The detected "API keys" like pk_test_direct_123 and sk_test_direct_123 are clearly placeholder values for unit testing, not actual secrets. No action needed.

Also applies to: 84-85, 148-149, 193-194, 253-254


170-187: Good verification of account ID handling.

These tests properly verify that oauth_account_id is loaded in OAuth mode and remains empty in direct mode, using reflection appropriately for testing private state.

Also applies to: 192-208


213-245: Thorough disconnect verification.

The test comprehensively verifies that all OAuth tokens (test and live) are properly cleared during disconnect.


284-297: Good security verification for disconnect URL.

This test confirms the disconnect URL contains proper nonce and routing parameters.

Comment on lines +101 to +112
add_filter('pre_http_request', function($preempt, $args, $url) {
if (strpos($url, '/oauth/init') !== false) {
return [
'response' => ['code' => 200],
'body' => wp_json_encode([
'oauthUrl' => 'https://connect.stripe.com/oauth/authorize?client_id=ca_test123&state=encrypted_state&scope=read_write',
'state' => 'test_state_123',
]),
];
}
return $preempt;
}, 10, 3);
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

Fix closure formatting to pass CI checks.

The multi-line closures violate project coding standards.

-		add_filter('pre_http_request', function($preempt, $args, $url) {
+		add_filter(
+			'pre_http_request',
+			function ($preempt, $args, $url) {
 			if (strpos($url, '/oauth/init') !== false) {
 				return [
-					'response' => ['code' => 200],
-					'body' => wp_json_encode([
-						'oauthUrl' => 'https://connect.stripe.com/oauth/authorize?client_id=ca_test123&state=encrypted_state&scope=read_write',
-						'state' => 'test_state_123',
-					]),
+					'response' => ['code' => 200],
+					'body'     => wp_json_encode(
+						[
+							'oauthUrl' => 'https://connect.stripe.com/oauth/authorize?client_id=ca_test123&state=encrypted_state&scope=read_write',
+							'state'    => 'test_state_123',
+						]
+					),
 				];
 			}
 			return $preempt;
-		}, 10, 3);
+			},
+			10,
+			3
+		);

Also applies to: 130-135

🧰 Tools
🪛 GitHub Check: Code Quality Checks

[warning] 107-107:
Array double arrow not aligned correctly; expected 4 space(s) between "'state'" and double arrow, but found 1.


[failure] 105-105:
Opening parenthesis of a multi-line function call must be the last content on the line


[warning] 105-105:
Array double arrow not aligned correctly; expected 5 space(s) between "'body'" and double arrow, but found 1.


[failure] 101-101:
Expected 1 space after FUNCTION keyword; 0 found


[failure] 101-101:
Only one argument is allowed per line in a multi-line function call


[failure] 101-101:
Opening parenthesis of a multi-line function call must be the last content on the line

🪛 PHPMD (2.15.0)

101-101: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
In tests/WP_Ultimo/Gateways/Stripe_OAuth_Test.php around lines 101-112 (and also
apply same change to 130-135), the multi-line anonymous function passed to
add_filter violates project coding standards; replace the multi-line closure
with a single-line closure (or a small named function) that performs the same
logic and returns the preempt value—ensure the entire callback fits on one line
and keep the 10, 3 parameters intact so CI linting passes.

- Remove platform_client_id property from Base_Stripe_Gateway
- Remove platform_client_id filter usage from OAuth E2E test
- Update test step numbering for clarity
- All 15 OAuth tests passing (44 assertions)

Platform credentials are kept secure on the proxy server and never
exposed in the distributed plugin code.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@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: 2

🧹 Nitpick comments (6)
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (3)

59-80: Test name vs assertion mismatch

The method name suggests asserting that the Stripe client is configured with the Stripe-Account header, but the test only verifies that oauth_account_id is loaded via reflection. Either:

  • Rename the test to reflect what it actually verifies (e.g. test_oauth_account_id_loaded_from_settings), or
  • Extend it to inject a mock StripeClient via set_stripe_client() and assert that calls are made with the expected connected account context.

128-142: Consider clearing sandbox mode and transient OAuth state in test reset

clear_all_stripe_settings() wipes most Stripe-related options but leaves stripe_sandbox_mode and wu_stripe_oauth_state untouched. For more deterministic tests (and to future‑proof against new tests that don’t explicitly set sandbox mode/state), consider resetting those here as well.


52-56: Style warnings about equals alignment

Static analysis is flagging equals‑sign alignment on some assignments (e.g., Lines 53, 75, 107). If you’re aiming for a clean PHPCS run, align these per the project’s coding standard or disable the rule locally if alignment isn’t desired.

Also applies to: 74-80, 108-112

inc/gateways/class-base-stripe-gateway.php (3)

84-115: OAuth state fields and defaults are consistent with dual‑mode design

$is_connect_enabled, $oauth_access_token, $oauth_account_id, and $authentication_mode provide a clear separation between direct and OAuth modes, and defaulting authentication_mode to 'direct' preserves existing behavior. You might optionally set $is_connect_enabled = false explicitly in the non‑OAuth branches of setup_api_keys() to make the state resets more obvious when reconnect flows evolve, but functionally this is fine.


275-309: Proxy URL and business metadata

Using a filterable proxy URL (wu_stripe_connect_proxy_url) is a good escape hatch for staging/self‑hosting. get_business_data() is fine as a first pass; as a future enhancement you may want to pull country from site or network settings instead of hard‑coding 'US'.


517-560: Disconnect flow and cleanup are sensible

  • Notifying the proxy via a non‑blocking POST to /deauthorize keeps admin UX responsive.
  • Clearing both test and live OAuth tokens while leaving direct keys intact matches the intended “fallback to direct mode after disconnect” behavior.
  • Redirecting back to settings with a stripe_disconnected=1 flag is a clean way to show a confirmation notice.

You might optionally also clear any lingering wu_stripe_oauth_state option here, though exchange_code_for_keys() already deletes it on successful connect.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd87731 and c431f91.

📒 Files selected for processing (2)
  • inc/gateways/class-base-stripe-gateway.php (5 hunks)
  • tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
inc/gateways/class-base-stripe-gateway.php (3)
inc/functions/settings.php (2)
  • wu_get_setting (37-40)
  • wu_save_setting (51-54)
inc/functions/string-helpers.php (1)
  • wu_replace_dashes (50-53)
inc/gateways/class-base-gateway.php (1)
  • get_id (196-199)
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (2)
inc/functions/settings.php (1)
  • wu_save_setting (51-54)
inc/gateways/class-base-stripe-gateway.php (2)
  • is_using_oauth (271-273)
  • get_authentication_mode (261-263)
🪛 GitHub Check: Code Quality Checks
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php

[warning] 107-107:
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space


[warning] 75-75:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space


[warning] 53-53:
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

🪛 PHPMD (2.15.0)
inc/gateways/class-base-stripe-gateway.php

317-317: Avoid unused parameters such as '$state'. (undefined)

(UnusedFormalParameter)

⏰ 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). (4)
  • GitHub Check: PHP 8.4
  • GitHub Check: cypress (8.1, chrome)
  • GitHub Check: cypress (8.2, chrome)
  • GitHub Check: Build Plugin for Testing
🔇 Additional comments (7)
tests/WP_Ultimo/Gateways/Stripe_OAuth_E2E_Test.php (2)

35-56: Good coverage of OAuth token loading and mode detection

This test correctly simulates the callback state and asserts both is_using_oauth() and oauth_account_id wiring; it matches the new setup_api_keys() behavior.


85-123: Nice end‑to‑end simulation of OAuth → direct fallback

The flow simulation (connect via OAuth, then clear tokens and fall back to direct keys) lines up with setup_api_keys() and is_using_oauth(). This is a solid safety net for regressions in auth mode switching.

inc/gateways/class-base-stripe-gateway.php (5)

132-147: Stripe client configuration for Connect accounts

Using a single StripeClient instance with 'api_key' => $this->secret_key and 'stripe_account' => $this->oauth_account_id when is_using_oauth() is true is the right pattern for directing calls to a connected account. Just be aware that once the client is instantiated, subsequent changes to $secret_key or $oauth_account_id in the same request will not be reflected unless you reset $this->stripe_client.

If there’s any doubt, please confirm in the Stripe PHP client docs for your pinned version that 'stripe_account' is still the correct config key for defaulting the Stripe-Account header.


207-253: Dual‑mode key setup (OAuth vs direct) looks correct

setup_api_keys() now:

  • Prefers {id}_test/live_access_token and related OAuth fields when present.
  • Falls back to {id}_test/live_pk_key and {id}_test/live_sk_key otherwise.
    It also keeps Stripe’s global API key in sync with $this->secret_key, so downstream calls use the right credentials. This matches how the new tests exercise mode switching.

255-273: Auth mode helpers are simple and work well with tests/UI

get_authentication_mode() and is_using_oauth() (requiring both 'oauth' mode and $is_connect_enabled) provide a clean way for both tests and UI to differentiate modes. This aligns with the new E2E tests and should make future feature flags straightforward.


364-392: Local init/disconnect URLs are straightforward and nonce‑protected

get_oauth_init_url() and get_disconnect_url() generate admin URLs with dedicated nonces, keeping the actual proxy traffic deferred until the user clicks the actions. This is a good pattern for avoiding background proxy calls on every settings page load.


400-433: Callback handling is mostly solid; ensure it only runs in the intended admin context

  • All $_GET inputs that are used beyond simple existence checks are sanitized via sanitize_text_field( wp_unslash( … ) ), and OAuth callback CSRF is guarded via the stored wu_stripe_oauth_state.
  • OAuth init and disconnect flows are nonce‑protected and restricted to page=wu-settings.

Just confirm that handle_oauth_callbacks() is only hooked on admin requests (e.g., admin_init or similar) and not on front‑end traffic, to avoid unnecessary proxy calls or wp_die() surprises in non‑admin contexts.

Comment on lines +317 to +353
public function get_connect_authorization_url(string $state = ''): string {
$proxy_url = $this->get_proxy_url();
$return_url = admin_url('admin.php?page=wu-settings&tab=payment-gateways');

// Call proxy to initialize OAuth
$response = wp_remote_post(
$proxy_url . '/oauth/init',
[
'body' => wp_json_encode(
[
'returnUrl' => $return_url,
'businessData' => $this->get_business_data(),
'testMode' => $this->test_mode,
]
),
'headers' => [
'Content-Type' => 'application/json',
],
'timeout' => 30,
]
);

if (is_wp_error($response)) {
return '';
}

$data = json_decode(wp_remote_retrieve_body($response), true);

if (empty($data['oauthUrl'])) {
return '';
}

// Store state for verification
update_option('wu_stripe_oauth_state', $data['state'], false);

return $data['oauthUrl'];
}
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 | 🟠 Major

Harden handling of proxy /oauth/init responses and unused $state

Two points here:

  1. Potential fatal on invalid JSON or unexpected body

    • $data = json_decode(..., true); is used immediately as an array in empty($data['oauthUrl']) and later in $data['state'].
    • If the proxy (or an upstream layer/WAF) ever returns non‑JSON or an unexpected payload, this can become a fatal error on PHP 8+ instead of a graceful failure.

    Consider guarding the decoded body and verifying required keys:

    -	$data = json_decode(wp_remote_retrieve_body($response), true);
    -
    -	if (empty($data['oauthUrl'])) {
    -		return '';
    -	}
    -
    -	// Store state for verification
    -	update_option('wu_stripe_oauth_state', $data['state'], false);
    +	$body = wp_remote_retrieve_body($response);
    +	$data = json_decode($body, true);
    +
    +	if (! is_array($data) || empty($data['oauthUrl']) || empty($data['state'])) {
    +		return '';
    +	}
    +
    +	// Store state for verification
    +	update_option('wu_stripe_oauth_state', $data['state'], false);

    Optionally also check wp_remote_retrieve_response_code($response) and treat non‑2xx codes as failure before decoding.

  2. Unused $state parameter

    • $state is intentionally unused for BC, but PHPMD flags it. To keep tools quiet without changing behavior, you can either add an inline ignore (// @phpstan-ignore-next-line / //phpmd:ignore UnusedFormalParameter) or explicitly unset($state); with a short comment.
🧰 Tools
🪛 PHPMD (2.15.0)

317-317: Avoid unused parameters such as '$state'. (undefined)

(UnusedFormalParameter)

Comment on lines +442 to +509
protected function exchange_code_for_keys(string $encrypted_code): void {
$proxy_url = $this->get_proxy_url();

// Call proxy to exchange code for keys
$response = wp_remote_post(
$proxy_url . '/oauth/keys',
[
'body' => wp_json_encode(
[
'code' => $encrypted_code,
'testMode' => $this->test_mode,
]
),
'headers' => [
'Content-Type' => 'application/json',
],
'timeout' => 30,
]
);

if (is_wp_error($response)) {
wp_die(esc_html__('Failed to connect to proxy server', 'ultimate-multisite'));
}

$status_code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);

if (200 !== $status_code) {
wp_die(esc_html__('Failed to obtain access token', 'ultimate-multisite'));
}

$data = json_decode($body, true);

if (empty($data['accessToken']) || empty($data['accountId'])) {
wp_die(esc_html__('Invalid response from proxy', 'ultimate-multisite'));
}

// Delete state after successful exchange
delete_option('wu_stripe_oauth_state');

$id = wu_replace_dashes($this->get_id());

// Save tokens
if ($this->test_mode) {
wu_save_setting("{$id}_test_access_token", $data['secretKey']);
wu_save_setting("{$id}_test_refresh_token", $data['refreshToken'] ?? '');
wu_save_setting("{$id}_test_account_id", $data['accountId']);
wu_save_setting("{$id}_test_publishable_key", $data['publishableKey']);
} else {
wu_save_setting("{$id}_live_access_token", $data['secretKey']);
wu_save_setting("{$id}_live_refresh_token", $data['refreshToken'] ?? '');
wu_save_setting("{$id}_live_account_id", $data['accountId']);
wu_save_setting("{$id}_live_publishable_key", $data['publishableKey']);
}

// Redirect back to settings
$redirect_url = add_query_arg(
[
'page' => 'wu-settings',
'tab' => 'payment-gateways',
'stripe_connected' => '1',
],
admin_url('admin.php')
);

wp_safe_redirect($redirect_url);
exit;
}
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 | 🟠 Major

Robustness of /oauth/keys exchange and response handling

A few details worth tightening here:

  1. JSON decode and response‑shape validation

    • As with /oauth/init, $data = json_decode($body, true); is used immediately as an array. An HTML error page or invalid JSON will cause a fatal before you hit the wp_die('Invalid response from proxy') branch.

    Consider:

    -	$data = json_decode($body, true);
    -
    -	if (empty($data['accessToken']) || empty($data['accountId'])) {
    -		wp_die(esc_html__('Invalid response from proxy', 'ultimate-multisite'));
    -	}
    +	$data = json_decode($body, true);
    +
    +	if (! is_array($data) || empty($data['accountId']) || empty($data['secretKey']) || empty($data['publishableKey'])) {
    +		wp_die(esc_html__('Invalid response from proxy', 'ultimate-multisite'));
    +	}
  2. accessToken vs secretKey mismatch

    • The validity check currently requires $data['accessToken'], but you only persist $data['secretKey'] into {id}_*_access_token options and never use accessToken afterward.
    • If the proxy were ever to omit accessToken while still providing secretKey/accountId, this would incorrectly trigger the “Invalid response from proxy” path.

    Aligning the validation with the fields you actually use (notably secretKey, publishableKey, and accountId) will make this more robust and self‑consistent.

  3. HTTP status handling is good, but consider surfacing more detail

    • You already check for non‑200 responses and call wp_die('Failed to obtain access token'). That’s fine for now; if support needs increase, you might later log $status_code and a trimmed $body (or a proxy‑provided error message) via wu_log_add() for easier debugging.
🤖 Prompt for AI Agents
In inc/gateways/class-base-stripe-gateway.php lines 442-509, the code assumes
json_decode($body, true) returns a valid array and validates on accessToken
while later using secretKey; update the handling to first decode and verify the
result is an array (check json_last_error() or is_array) and call wp_die with a
clear message if decoding fails, then change the required fields check to
validate the actual fields you persist/use (secretKey, publishableKey, and
accountId) and accept refreshToken as optional; keep existing status/error
wp_die paths but consider logging status/body for diagnostics if desired.

@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: 0

♻️ Duplicate comments (3)
assets/js/gateways/stripe.js (1)

38-51: ** Async promise executor swallows errors.**

This pattern was flagged in a previous review and remains unresolved. The async executor with an empty catch block and unconditional resolve() will hide validation failures.

🔎 Recommended fix
-				promises.push(
-					new Promise(async (resolve, reject) => {
-						try {
-							// Validate the Payment Element before submission
-							const { error } = await elements.submit();
-
-							if (error) {
-								reject(error);
-							} else {
-								resolve();
-							}
-						} catch (err) {
-							reject(err);
-						}
-					})
-				);
+				promises.push(
+					elements.submit().then(({ error }) => {
+						if (error) {
+							return Promise.reject(error);
+						}
+					})
+				);
inc/gateways/class-base-stripe-gateway.php (2)

317-353: ** Harden JSON decode and handle malformed proxy responses.**

This concern was flagged in a previous review and remains unresolved. If the proxy returns non-JSON or an unexpected structure, $data['oauthUrl'] and $data['state'] access can fatal on PHP 8+.

🔎 Recommended hardening
 	$data = json_decode(wp_remote_retrieve_body($response), true);
 
-	if (empty($data['oauthUrl'])) {
+	if (! is_array($data) || empty($data['oauthUrl']) || empty($data['state'])) {
 		return '';
 	}
 
 	// Store state for verification
 	update_option('wu_stripe_oauth_state', $data['state'], false);

Optionally also check wp_remote_retrieve_response_code($response) before decoding.


442-509: ** Strengthen /oauth/keys response validation.**

As noted in a previous review, the code validates $data['accessToken'] but only uses $data['secretKey']. If the proxy omits accessToken while providing the other fields, this incorrectly triggers the error path. Additionally, the JSON decode lacks array validation.

🔎 Recommended validation improvements
 	$data = json_decode($body, true);
 
-	if (empty($data['accessToken']) || empty($data['accountId'])) {
+	if (! is_array($data) || empty($data['accountId']) || empty($data['secretKey']) || empty($data['publishableKey'])) {
 		wp_die(esc_html__('Invalid response from proxy', 'ultimate-multisite'));
 	}

This aligns validation with the fields actually persisted (secretKey, publishableKey, accountId) and handles malformed responses gracefully.

🧹 Nitpick comments (1)
inc/managers/class-gateway-manager.php (1)

739-774: Scheduling logic is appropriate but has unused parameters.

The method correctly schedules verification only for pending Stripe payments and uses the gateway's scheduling method. However, PHPMD flags $customer, $cart, and $type as unused. Since this is a hook handler matching a specific signature, these are acceptable (the hook provides them).

If you want to silence the static analysis warnings without changing behavior, you can add a comment:

// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

Alternatively, explicitly unset them with a brief comment explaining they're required by the hook signature.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c431f91 and 44f23c3.

📒 Files selected for processing (6)
  • assets/js/gateways/stripe.js
  • inc/admin-pages/class-wizard-admin-page.php
  • inc/checkout/class-checkout-pages.php
  • inc/gateways/class-base-stripe-gateway.php
  • inc/gateways/class-stripe-gateway.php
  • inc/managers/class-gateway-manager.php
🧰 Additional context used
🧬 Code graph analysis (5)
inc/managers/class-gateway-manager.php (4)
inc/functions/helper.php (2)
  • wu_request (132-137)
  • wu_log_add (208-211)
inc/models/class-payment.php (3)
  • get_status (500-503)
  • get_gateway (524-527)
  • get_membership (278-281)
inc/database/payments/class-payment-status.php (1)
  • Payment_Status (22-104)
inc/gateways/class-base-stripe-gateway.php (2)
  • verify_and_complete_payment (3376-3473)
  • schedule_payment_verification (3487-3503)
inc/gateways/class-base-stripe-gateway.php (4)
inc/functions/settings.php (2)
  • wu_get_setting (37-40)
  • wu_save_setting (51-54)
inc/functions/string-helpers.php (1)
  • wu_replace_dashes (50-53)
inc/admin-pages/class-base-admin-page.php (1)
  • get_id (236-239)
inc/gateways/class-base-gateway.php (1)
  • get_id (196-199)
assets/js/gateways/stripe.js (1)
assets/js/checkout.js (1)
  • promises (864-864)
inc/gateways/class-stripe-gateway.php (2)
inc/functions/settings.php (1)
  • wu_register_settings_field (88-91)
inc/gateways/class-base-stripe-gateway.php (3)
  • is_using_oauth (271-273)
  • get_disconnect_url (382-392)
  • get_oauth_init_url (364-374)
inc/checkout/class-checkout-pages.php (5)
inc/functions/helper.php (1)
  • wu_request (132-137)
inc/managers/class-gateway-manager.php (1)
  • get_gateway (473-476)
inc/models/class-membership.php (2)
  • get_gateway (1642-1645)
  • get_status (1546-1549)
inc/models/class-payment.php (2)
  • get_gateway (524-527)
  • get_status (500-503)
inc/database/payments/class-payment-status.php (1)
  • Payment_Status (22-104)
🪛 Biome (2.1.2)
assets/js/gateways/stripe.js

[error] 38-51: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

🪛 PHPMD (2.15.0)
inc/managers/class-gateway-manager.php

751-751: Avoid unused parameters such as '$customer'. (undefined)

(UnusedFormalParameter)


751-751: Avoid unused parameters such as '$cart'. (undefined)

(UnusedFormalParameter)


751-751: Avoid unused parameters such as '$type'. (undefined)

(UnusedFormalParameter)

inc/gateways/class-base-stripe-gateway.php

317-317: Avoid unused parameters such as '$state'. (undefined)

(UnusedFormalParameter)

⏰ 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). (3)
  • GitHub Check: cypress (8.1, chrome)
  • GitHub Check: cypress (8.2, chrome)
  • GitHub Check: PHP 8.0
🔇 Additional comments (17)
inc/admin-pages/class-wizard-admin-page.php (1)

208-222: LGTM — clean extraction of CSS classes into a helper method.

The refactor improves maintainability by centralizing the wrapper classes. No functional change.

inc/gateways/class-stripe-gateway.php (4)

48-49: LGTM — OAuth callback handling properly wired.

The admin_init hook correctly delegates OAuth flow handling to the base class method. Placement and approach look good.


104-150: OAuth UI fields look solid.

The new authentication section, connection status block, and direct keys toggle provide a clear UI flow. The visibility conditions (require arrays) properly gate fields, and all strings are internationalized.


745-751: Payment Element container migration looks correct.

The shift from card-element and card-errors to payment-element and payment-errors aligns with the Stripe Payment Element API. This matches the JavaScript changes that mount the Payment Element.


852-899: OAuth connection status HTML rendering is well-structured.

The method provides clear visual feedback for connected vs. disconnected states, displays the account ID, and uses proper escaping. The use of Tailwind utility classes and dashicons keeps the UI consistent.

assets/js/gateways/stripe.js (2)

130-256: Payment Element initialization and mode switching logic looks solid.

The dynamic mode selection (payment vs. setup) based on order total, reinitialization on mode/amount changes, and cleanup logic are all well-structured. The use of currentElementsMode and currentElementsAmount prevents unnecessary reinitializations.


284-331: Style synchronization properly uses elements.update().

The use of elements.update({ appearance: { ... } }) (line 315) is correct for updating Payment Element appearance at runtime, as confirmed by Stripe documentation. This resolves concerns from the previous review.

inc/gateways/class-base-stripe-gateway.php (4)

127-147: Stripe-Account header correctly injected for OAuth mode.

The conditional stripe_account header configuration ensures API calls are properly scoped to the connected account. Implementation aligns with Stripe Connect best practices.


207-253: OAuth token preference and authentication mode detection look solid.

The dual-mode authentication (OAuth preferred, direct keys fallback) is correctly implemented. Token and key retrieval logic properly handles both test and live modes, and the authentication mode is set appropriately.


3365-3473: Fallback payment verification logic is well-designed.

The method correctly handles setup vs. payment intents, validates payment status before attempting verification, checks Stripe intent status, and completes the payment locally when succeeded. Error handling and logging are appropriate.


3486-3503: Scheduled verification properly uses Action Scheduler.

The method schedules a fallback verification job with deduplication (checks if already scheduled) and a configurable delay. Integration with the verification handler is clean.

inc/managers/class-gateway-manager.php (3)

116-130: New verification endpoints and hooks properly wired.

The AJAX endpoint (wu_check_payment_status), scheduled verification handler (wu_verify_stripe_payment), and checkout hook (wu_checkout_done) are correctly registered and aligned with the fallback verification architecture.


587-667: AJAX payment status check is well-structured.

The handler validates the payment hash, retrieves the payment, handles completion, determines the gateway (with membership fallback), restricts verification to Stripe gateways, and delegates to the gateway's verification method. Error paths and success responses are appropriate.


669-737: Scheduled verification handler is robust.

The method supports both legacy (single arg) and new (array) formats for backward compatibility, validates payment status, determines the gateway with fallback logic, restricts to Stripe family gateways, and logs outcomes appropriately.

inc/checkout/class-checkout-pages.php (3)

38-41: Payment status polling hook properly registered.

The wp_enqueue_scripts hook correctly wires the polling script enqueue logic for the thank you page.


212-213: Error message aliases provide compatibility.

The invalid_key and expired_key aliases map to the existing invalidkey and expiredkey messages, ensuring backward compatibility for password reset flows.


676-777: Payment status polling implementation is well-designed.

The method correctly:

  • Identifies the thank you page context (payment hash + status=done)
  • Restricts polling to pending Stripe payments
  • Provides gateway fallback via membership
  • Localizes configuration (interval, max attempts, messages)
  • Adds appropriate inline CSS for status states

The polling provides a user-friendly fallback when webhooks are delayed.

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.

2 participants