Skip to content

Fraud Protection: Add Blackbox integration for PayPal express checkout#31

Merged
luizreis merged 4 commits intotrunkfrom
woosubs-1447-paypal-express-integration
Mar 13, 2026
Merged

Fraud Protection: Add Blackbox integration for PayPal express checkout#31
luizreis merged 4 commits intotrunkfrom
woosubs-1447-paypal-express-integration

Conversation

@luizreis
Copy link
Contributor

@luizreis luizreis commented Mar 5, 2026

Changes proposed in this Pull Request:

PayPal express checkout (product page, cart, mini-cart buttons) bypasses the standard WC checkout pipeline entirely — our checkout protectors never fire. This PR adds Blackbox fraud protection to these flows.

How it works:

  1. A JS fetch interceptor (paypal-express.js) patches window.fetch to inject the Blackbox session ID into ppc-create-order requests and reset Blackbox after the response (same pattern PayPal uses for its own reCAPTCHA module).
  2. PayPalCompat hooks woocommerce_paypal_payments_create_order_request_started to verify the session before PayPal creates the order. On BLOCK, responds with wp_send_json_error(403).
  3. Blackbox scripts are enqueued on product/cart pages when a PayPal gateway is available (in addition to the existing checkout/pay-for-order/add-payment-method pages).

Skipping redundant verification:

Smart-button PayPal flows (express buttons, card fields, wallets) always go through ppc-create-order where PayPalCompat verifies the session. Standard protectors (BlocksCheckoutProtector, ShortcodeCheckoutProtector) may also fire later in the same payment flow — these are redundant and should be skipped.

A woocommerce_fraud_protection_skip_session_verify filter in SessionVerifier lets extensions request skipping. PayPalCompat hooks it to skip when:

  • Same request: The current request IS ppc-create-order — PayPal's CreateOrderEndpoint calls CheckoutFormValidator::validate() which fires woocommerce_checkout_process, triggering ShortcodeCheckoutProtector before our verify action runs. Skip — PayPalCompat handles it next.
  • Same session ID: The Blackbox session ID matches one already verified during ppc-create-order — for card/wallet flows on blocks checkout, blocks-checkout.js captures the session ID before PayPal's createOrder callback fires ppc-create-order, so both calls use the same ID.
  • Approved order in session: An approved PayPal order exists in the WC session (ppcp.order) — post-approval express flow, already verified at CreateOrder (Blackbox was reset since, so session IDs won't match, but the session flag catches it).

Flows that do NOT go through ppc-create-order (Blocks "Place Order" with ppcp-gateway server-side redirect, APM gateways) are NOT skipped.

PayPal payment flow coverage (more details on the docs repo):

Flow Goes through ppc-create-order? Verified by
Express buttons (product/cart/mini-cart) Yes PayPalCompat
Classic checkout smart buttons Yes (with early form validation) PayPalCompat
Card payments (hosted fields, card button) Yes PayPalCompat
Digital wallets (Apple Pay, Google Pay) Yes PayPalCompat
Fastlane (AXO gateway) Yes PayPalCompat
Blocks "Place Order" with PayPal No (server-side order creation) BlocksCheckoutProtector
Local APMs (iDEAL, P24, BLIK, etc.) No (own process_payment()) Standard protectors

Not yet covered (out of scope for this PR): PayPal payment data resolution (card details, wallet/payer identity).

Closes WOOSUBS-1447

paypal-verify-integration-demo.mov

How to test the changes in this Pull Request:

Prerequisites

  • PayPal Payments plugin active with sandbox credentials
  • PayPal express buttons enabled on product and cart pages (PayPal Payments > Settings > Standard Payments)
  • A local APM enabled, such as iDEAL (Settings > PayPal Payments > Payment Methods). Your store currency needs to be EUR to use iDEAL
  • Google Pay or Apple Pay enabled (Settings > PayPal Payments > Payment Methods). Make sure the buttons are also displayed (Settings > PayPal Payments > Styling)

Test 1: Express from product page

  1. Go to a product page
  2. Click the PayPal express button
  3. Complete the PayPal sandbox login and approve
  4. The order should complete successfully
  5. Check woo-fraud-protection logs: you should see a paypal_express_order_creation verify with a session ID

Test 2: Express from cart page

  1. Add a product to cart, go to the cart page
  2. Click the PayPal express button
  3. Complete PayPal approval
  4. The order should complete
  5. Check woo-fraud-protection logs: you should see a paypal_express_order_creation verify with a session ID, then a Session verification skipped by ... filter for source: blocks_checkout message (the skip means the redundant verify from BlocksCheckoutProtector was correctly suppressed)

Test 3: Express from classic checkout page

  1. Switch to the classic checkout shortcode (if not already using it)
  2. Go to checkout — PayPal renders a smart button that replaces the "Place Order" button
  3. Click the PayPal smart button, complete approval
  4. The order should complete
  5. Check logs: paypal_express_order_creation verify, then one or two Session verification skipped ... for source: shortcode_checkout messages (PayPal's CreateOrderEndpoint may trigger woocommerce_checkout_process during form validation and again on approval — both are correctly skipped)

Test 4: Digital wallet (Apple Pay or Google Pay)

  1. Go to a product or cart page where Apple Pay / Google Pay buttons are visible
  2. Click the wallet button and complete payment via the platform sheet
  3. Check logs: paypal_express_order_creation verify with a session ID. If the payment completes successfully, you should also see a skip message (same as Test 1).

Test 5: Blocks checkout — "Place Order" with card (Debit & Credit Cards)

  1. Go to the blocks checkout page
  2. Select "Debit & Credit Cards" as the payment method
  3. Fill in card details (sandbox test card: 4032039317984658, any future expiry, any CVV)
  4. Click "Place Order"
  5. Check logs: paypal_express_order_creation verify (card flows go through ppc-create-order), then a Session verification skipped ... for source: blocks_checkout message — the skip works because blocks-checkout.js captured the same Blackbox session ID before ppc-create-order ran, and the session ID match triggers the skip

Test 6: Blocks checkout — "Place Order" with PayPal (non-express)

  1. Go to the blocks checkout page
  2. Fill in checkout fields, select PayPal from the payment method radio buttons
  3. Click "Place Order" — do NOT use the express PayPal button that appears at the top of the page
  4. Complete PayPal approval in the redirect flow
  5. Check logs: you should see a blocks_checkout verify with a valid session ID — there should be NO skip message before it (this flow doesn't go through ppc-create-order, so it's verified normally by BlocksCheckoutProtector)

Test 7: Local APM checkout (e.g. iDEAL)

  1. Go to checkout (blocks or classic), set billing country to NL
  2. Select iDEAL as the payment method
  3. Complete the payment
  4. Check logs: you should see a normal shortcode_checkout or blocks_checkout verify with a valid session ID and the ppcp-ideal payment method — there should be NO skip message (APMs don't go through ppc-create-order, so standard protectors verify normally)

Test 8: Non-PayPal gateway unaffected

  1. Go to checkout (blocks or classic)
  2. Pay with a non-PayPal gateway (e.g. Stripe, direct bank transfer)
  3. Check logs: you should see a normal verify for the checkout type — there should be NO skip message (the filter only affects PayPal gateways)

Test 9: Blocked session on express checkout

  1. Add a decision override filter to force a block: add_filter( 'woocommerce_fraud_protection_decision', fn() => 'block' );
  2. Click a PayPal express button on a product or cart page
  3. The PayPal popup should NOT open (or should close immediately) and an error message should appear
  4. Check logs: paypal_express_order_creation verify with a block decision
  5. Remove the filter after testing

Test 10: Blackbox scripts on product/cart pages

  1. Go to a product page — check browser DevTools (Network tab) for blackbox-init.js and paypal-express.js loading
  2. Go to the cart page — same scripts should load
  3. Go to a non-checkout/non-product page (e.g. "About Us") — scripts should NOT load

PayPal express checkout (product page, cart, mini-cart) bypasses the
standard WC checkout pipeline. Hook into PayPal's CreateOrder AJAX
endpoint to verify sessions before PayPal order creation.

JS fetch interceptor injects the Blackbox session ID into
ppc-create-order requests (same pattern as PayPal's reCAPTCHA module)
and resets Blackbox after the fetch returns so subsequent payment
attempts get a fresh session for evaluation.
@luizreis luizreis force-pushed the woosubs-1447-paypal-express-integration branch 3 times, most recently from e73871a to 2853976 Compare March 6, 2026 16:43
PayPal checkout flows trigger double verification: PayPalCompat
verifies during ppc-create-order (with session_id), then after
approval, the standard protector fires again with an empty session_id
(PayPal submits the checkout request directly, bypassing checkout JS).

Add a `woocommerce_fraud_protection_should_verify_session` filter in
SessionVerifier so extensions can skip redundant verify calls.
PayPalCompat hooks this filter to skip verification when the payment
method is a ppcp-* gateway AND either:
- An approved PayPal order exists in the WC session (post-approval
  flow already verified at CreateOrder), or
- The current request is ppc-create-order itself (PayPal's form
  validation fires the shortcode protector before our verify runs).

Regular checkout with PayPal (e.g. Blocks "Place Order" without
prior approval) is not skipped.
@luizreis luizreis force-pushed the woosubs-1447-paypal-express-integration branch from 2853976 to 781afe9 Compare March 6, 2026 17:12
@luizreis luizreis marked this pull request as ready for review March 6, 2026 17:18
@luizreis luizreis requested a review from leonardola March 6, 2026 17:18
Copy link
Contributor

@leonardola leonardola left a comment

Choose a reason for hiding this comment

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

Test 1: Express from product page

I do see the paypal_express_order_creation being logged but I don't see the skip message. Weirdly on test 2 I do see the skip message.

Test 3: Express from classic checkout page

I don't see the paypal_express_order_creation but I do see woocommerce_checkout_order_processed

Before I continue would you check what could be going on?

@luizreis
Copy link
Contributor Author

Sure!

Test 1: Express from product page

I do see the paypal_express_order_creation being logged but I don't see the skip message. Weirdly on test 2 I do see the skip message.

That should be okay, I updated the testing instructions to reflect that. The skip message happens when PayPal tries to call ppc-create-order and checkout at the same time, so what we don't want to see is a paypal_express_order_creation followed by another verify source.

Test 3: Express from classic checkout page

I don't see the paypal_express_order_creation but I do see woocommerce_checkout_order_processed

That's weird. Are there any verify calls happening (or being skipped) with other sources? Do you see a checkout button similar to the one in the video (around 1:20), or is it different?

@leonardola
Copy link
Contributor

Do you see a checkout button similar to the one in the video (around 1:20), or is it different?

Yep.

Screenshot 2026-03-10 at 15 54 13

I'm seeing the following error being logged when it tries to verify:
ERROR Blackbox API request failed: Blackbox API POST /verify returned status code 400.

Copy link
Contributor

@leonardola leonardola left a comment

Choose a reason for hiding this comment

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

Overall the changes look good. But I left a few comments and I couldn't correctly test the Apple/Google pay flows

Test 2: Express from cart page

I see the skipped log for blocks cart page but not for the shortcode cart page.

Test 4: Digital wallet (Apple Pay or Google Pay

I could not enable Apple or Google pay. An error happens on PayPal side which ends up blocking the setup.

// Stored regardless of decision: Blackbox sessions are single-use, so
// re-verifying the same ID would fail rather than produce a fresh verdict.
if ( '' !== $session_id && function_exists( 'WC' ) && WC()->session ) {
WC()->session->set( self::VERIFIED_SESSION_ID_KEY, $session_id );
Copy link
Contributor

Choose a reason for hiding this comment

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

I've also save the session key on the report flow PR #34 here let's agree on a naming convention and use the same in both PRs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think these should be separate as they are serving different purposes: _fraud_protection_paypal_verified_session_id is an ephemeral WC session data to be used by the PayPal compat within a single checkout to detect redundant verification between ppc-create-order and the standard checkout protector, then it's irrelevant.

IMO, we shouldn't use the same key, but I'm happy to align the prefix if you'd like.

The fetch API accepts string, URL, or Request objects as the resource
parameter. Use instanceof Request for Request objects and String() for
URL objects to correctly extract the URL for interception matching.
@luizreis luizreis merged commit df01e85 into trunk Mar 13, 2026
18 checks passed
@luizreis luizreis deleted the woosubs-1447-paypal-express-integration branch March 13, 2026 17:26
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