Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9780c40
Add hook to allow for custom webhook post-processing
daledupreez Jul 7, 2025
3331c99
Merge branch 'develop' into add/hook-for-inbound-webhooks
wjrosa Jul 18, 2025
aa7989c
Moving action inside specific methods when processing async events
wjrosa Jul 18, 2025
5866fb2
Adding a try...catch block
wjrosa Jul 18, 2025
e991d11
Changelog and readme entries
wjrosa Jul 18, 2025
c52d7a1
Update includes/class-wc-stripe-webhook-handler.php
wjrosa Jul 21, 2025
c8bd4ba
Merge branch 'develop' into add/hook-for-inbound-webhooks
wjrosa Jul 21, 2025
09b792a
Merge branch 'develop' into add/hook-for-inbound-webhooks
wjrosa Jul 22, 2025
a63cbe1
Introducing a protected variable to store the order being processed
wjrosa Jul 22, 2025
cd17df2
Passing the whole notification object to deferred events
wjrosa Jul 22, 2025
acb696a
Fix tests
wjrosa Jul 22, 2025
bbc92e7
Merge branch 'develop' into add/hook-for-inbound-webhooks
wjrosa Jul 28, 2025
78cf5b4
Merge branch 'develop' into add/hook-for-inbound-webhooks
wjrosa Jul 29, 2025
91fdac9
Merge branch 'develop' into add/hook-for-inbound-webhooks
wjrosa Jul 29, 2025
c442208
Moving action after the deferred webhook switch
wjrosa Jul 29, 2025
29511dc
Merge branch 'develop' into add/hook-for-inbound-webhooks
diegocurbelo Jul 31, 2025
12bfe13
Merge branch 'develop' into add/hook-for-inbound-webhooks
wjrosa Aug 1, 2025
6f83270
Add try/catch to main hook; catch Throwable instead of Exception
daledupreez Aug 4, 2025
479c7b2
Refactor action triggers into helper function and add explicit docs f…
daledupreez Aug 4, 2025
c8e4b0a
Update changelog.txt
wjrosa Aug 4, 2025
09581c7
Readme entry update
wjrosa Aug 4, 2025
349378d
Update includes/class-wc-stripe-webhook-handler.php
wjrosa Aug 4, 2025
0b78f6f
Renaming the action to wc_stripe_webhook_received
wjrosa Aug 4, 2025
e6d8b32
Merge branch 'add/hook-for-inbound-webhooks' of https://github.com/wo…
wjrosa Aug 4, 2025
a6a1e14
Changelog and readme entries
wjrosa Aug 4, 2025
908dec4
Merge branch 'develop' into add/hook-for-inbound-webhooks
daledupreez Aug 18, 2025
353ff3e
Merge branch 'develop' into add/hook-for-inbound-webhooks
daledupreez Aug 19, 2025
be30ddd
Fix changelog entry location
daledupreez Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Tweak - Use more specific selector in express checkout e2e tests
* Tweak - Small improvements to e2e tests
* Fix - Fix unnecessary Stripe API calls when rendering subscription details
* Add - Adds a new action (`wc_stripe_webhook_received`) to allow additional actions to be taken for webhook notifications from Stripe

= 9.8.1 - 2025-08-15 =
* Fix - Remove connection type requirement from PMC sync migration attempt
Expand Down
101 changes: 95 additions & 6 deletions includes/class-wc-stripe-webhook-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ class WC_Stripe_Webhook_Handler extends WC_Stripe_Payment_Gateway {
*/
protected $deferred_webhook_action = 'wc_stripe_deferred_webhook';

/**
* The order object being processed.
*
* @var WC_Order|null
*/
protected $resolved_order = null;

/**
* Constructor.
*
Expand All @@ -71,7 +78,7 @@ public function __construct() {
// plugin when this code first appears.
WC_Stripe_Webhook_State::get_monitoring_began_at();

add_action( $this->deferred_webhook_action, [ $this, 'process_deferred_webhook' ], 10, 2 );
add_action( $this->deferred_webhook_action, [ $this, 'process_deferred_webhook' ], 10, 3 );
}

/**
Expand Down Expand Up @@ -269,6 +276,9 @@ public function process_webhook_payment( $notification, $retry = true ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

$order_id = $order->get_id();

$is_pending_receiver = ( 'receiver' === $notification->data->object->flow );
Expand Down Expand Up @@ -402,6 +412,9 @@ public function process_webhook_dispute( $notification ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

$this->set_stripe_order_status_before_hold( $order, $order->get_status() );

$needs_response = in_array( $notification->data->object->status, [ 'needs_response', 'warning_needs_response' ], true );
Expand Down Expand Up @@ -444,6 +457,9 @@ public function process_webhook_dispute_closed( $notification ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

if ( 'lost' === $status ) {
$message = __( 'The dispute was lost or accepted.', 'woocommerce-gateway-stripe' );
} elseif ( 'won' === $status ) {
Expand Down Expand Up @@ -495,6 +511,9 @@ public function process_webhook_capture( $notification ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

if ( WC_Stripe_Helper::payment_method_allows_manual_capture( $order->get_payment_method() ) ) {
$charge = $order->get_transaction_id();
$captured = $order->get_meta( '_stripe_charge_captured', true );
Expand Down Expand Up @@ -567,6 +586,9 @@ public function process_webhook_charge_succeeded( $notification ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

if ( ! $order->has_status( OrderStatus::ON_HOLD ) ) {
return;
}
Expand Down Expand Up @@ -625,6 +647,9 @@ public function process_webhook_charge_failed( $notification ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

// If order status is already in failed status don't continue.
if ( $order->has_status( OrderStatus::FAILED ) ) {
return;
Expand Down Expand Up @@ -665,6 +690,9 @@ public function process_webhook_source_canceled( $notification ) {
}
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

// Don't proceed if payment method isn't Stripe.
if ( 'stripe' !== $order->get_payment_method() ) {
WC_Stripe_Logger::log( 'Canceled webhook abort: Order was not processed by Stripe: ' . $order->get_id() );
Expand Down Expand Up @@ -697,6 +725,9 @@ public function process_webhook_refund( $notification ) {
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

if ( ! $order ) {
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
return;
Expand Down Expand Up @@ -785,6 +816,9 @@ public function process_webhook_refund_updated( $notification ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

$order_id = $order->get_id();

if ( 'stripe' === substr( (string) $order->get_payment_method(), 0, 6 ) ) {
Expand Down Expand Up @@ -888,6 +922,9 @@ public function process_review_opened( $notification ) {
}
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

$this->set_stripe_order_status_before_hold( $order, $order->get_status() );

$message = sprintf(
Expand Down Expand Up @@ -929,6 +966,9 @@ public function process_review_closed( $notification ) {
}
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

/* translators: 1) The reason type. */
$message = sprintf( __( 'The opened review for this order is now closed. Reason: (%s)', 'woocommerce-gateway-stripe' ), $notification->data->object->reason );

Expand Down Expand Up @@ -1047,6 +1087,9 @@ public function process_payment_intent( $notification ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

if ( $this->lock_order_payment( $order, $intent ) ) {
return;
}
Expand Down Expand Up @@ -1082,7 +1125,7 @@ public function process_payment_intent( $notification ) {
$process_webhook_async = apply_filters( 'wc_stripe_process_payment_intent_webhook_async', true, $order, $intent, $notification );
$is_awaiting_action = $order->get_meta( '_stripe_upe_waiting_for_redirect' ) ?? false;

// Process the webhook now if it's for a voucher or wallet payment , or if filtered to process immediately and order is not awaiting action.
// Process the webhook now if it's for a voucher or wallet payment, or if filtered to process immediately and order is not awaiting action.
if ( $is_voucher_payment || $is_wallet_payment || ( ! $process_webhook_async && ! $is_awaiting_action ) ) {
$charge = $this->get_latest_charge_from_intent( $intent );

Expand All @@ -1096,6 +1139,8 @@ public function process_payment_intent( $notification ) {

$charge->is_webhook_response = true;
$this->process_response( $charge, $order );

$this->run_webhook_received_action( (string) $notification->type, $notification, $this->resolved_order );
} else {
WC_Stripe_Logger::log( "Processing $notification->type ($intent->id) asynchronously for order $order_id." );

Expand All @@ -1112,7 +1157,6 @@ public function process_payment_intent( $notification ) {
do_action( 'wc_gateway_stripe_process_payment_intent_incomplete', $order );
}
}

break;
default:
if ( $is_voucher_payment && 'payment_intent.payment_failed' === $notification->type ) {
Expand Down Expand Up @@ -1153,6 +1197,9 @@ public function process_setup_intent( $notification ) {
return;
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

$allowed_payment_processing_statuses = [ OrderStatus::PENDING, OrderStatus::FAILED ];

$allowed_payment_processing_statuses = apply_filters_deprecated(
Expand Down Expand Up @@ -1227,8 +1274,9 @@ protected function defer_webhook_processing( $webhook_notification, $additional_
time() + $this->deferred_webhook_delay,
$this->deferred_webhook_action,
[
'type' => $webhook_notification->type,
'data' => $additional_data,
'type' => $webhook_notification->type,
'data' => $additional_data,
'notification' => $webhook_notification,
]
);
}
Expand All @@ -1240,8 +1288,9 @@ protected function defer_webhook_processing( $webhook_notification, $additional_
*
* @param string $webhook_type The webhook event name/type.
* @param array $additional_data Additional data passed to the scheduled job.
* @param stdClass $notification The webhook notification payload.
*/
public function process_deferred_webhook( $webhook_type, $additional_data ) {
public function process_deferred_webhook( $webhook_type, $additional_data, $notification = null ) {
try {
switch ( $webhook_type ) {
case 'payment_intent.succeeded':
Expand All @@ -1253,6 +1302,9 @@ public function process_deferred_webhook( $webhook_type, $additional_data ) {
throw new Exception( "Missing required data. 'order_id' is invalid or not found for the deferred '{$webhook_type}' event." );
}

// Set the order being processed for the `wc_stripe_webhook_received` action later.
$this->resolved_order = $order;

if ( empty( $intent_id ) ) {
throw new Exception( "Missing required data. 'intent_id' is missing for the deferred '{$webhook_type}' event." );
}
Expand All @@ -1269,6 +1321,8 @@ public function process_deferred_webhook( $webhook_type, $additional_data ) {
throw new Exception( "Unsupported webhook type: {$webhook_type}" );
break;
}

$this->run_webhook_received_action( (string) $webhook_type, $notification, $this->resolved_order );
} catch ( Exception $e ) {
WC_Stripe_Logger::log( 'Error processing deferred webhook: ' . $e->getMessage() );

Expand Down Expand Up @@ -1334,6 +1388,8 @@ public function process_account_updated( $notification ) {
public function process_webhook( $request_body ) {
$notification = json_decode( $request_body );

$this->resolved_order = null;

switch ( $notification->type ) {
case 'account.updated':
$this->process_account_updated( $notification );
Expand Down Expand Up @@ -1397,8 +1453,41 @@ public function process_webhook( $request_body ) {
$this->process_setup_intent( $notification );

}

// These events might be processed async. Skip the action trigger for them here. The trigger will be called inside the specific methods.
if ( 'payment_intent.succeeded' === $notification->type || 'payment_intent.amount_capturable_updated' === $notification->type ) {
return;
}

$this->run_webhook_received_action( $notification->type, $notification, $this->resolved_order );
}

/**
* Helper function to run the `wc_stripe_webhook_received` action consistently.
*
* @param string $webhook_type The type of webhook that was processed.
* @param object $notification The webhook data sent from Stripe.
* @param WC_Order|null $order The order being processed by the webhook.
*/
private function run_webhook_received_action( string $webhook_type, object $notification, ?WC_Order $order = null ): void {
try {
/**
* Fires after a webhook has been processed, but before we respond to Stripe.
* This allows for custom processing of the webhook after it has been processed.
* Note that the $order parameter may be null in various cases, especially when processing
* webhooks unrelated to orders, such as account updates.
*
* @since 9.8.0
*
* @param string $webhook_type The type of webhook that was processed.
* @param object $notification The webhook data sent from Stripe.
* @param WC_Order|null $order The order being processed by the webhook.
*/
do_action( 'wc_stripe_webhook_received', $webhook_type, $notification, $this->resolved_order );
} catch ( Throwable $e ) {
WC_Stripe_Logger::error( 'Error in wc_stripe_webhook_received action: ' . $e->getMessage(), [ 'error' => $e ] );
}
}
/**
* Fetches an order from a payment intent.
*
Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
* Tweak - Use more specific selector in express checkout e2e tests
* Tweak - Small improvements to e2e tests
* Fix - Fix unnecessary Stripe API calls when rendering subscription details
* Add - Adds a new action (`wc_stripe_webhook_received`) to allow additional actions to be taken for webhook notifications from Stripe

[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).
Loading
Loading