Skip to content

Commit 00afcc4

Browse files
Add Stripe App OAuth scheduled refresh connection (#3346)
* Add Stripe App OAuth scheduled refresh connection * Unschedule connection refresh when saving keys for connect * Remove connection_type and refresh_token from settings when disconnecting --------- Co-authored-by: James Allan <[email protected]>
1 parent 9c023b7 commit 00afcc4

File tree

3 files changed

+138
-1
lines changed

3 files changed

+138
-1
lines changed

includes/admin/class-wc-rest-stripe-account-keys-controller.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,11 @@ public function set_account_keys( WP_REST_Request $request ) {
265265
&& ! $settings['test_secret_key'];
266266

267267
if ( $is_deleting_account ) {
268-
$settings['enabled'] = 'no';
268+
$settings['enabled'] = 'no';
269+
$settings['connection_type'] = '';
270+
$settings['test_connection_type'] = '';
271+
$settings['refresh_token'] = '';
272+
$settings['test_refresh_token'] = '';
269273
$this->record_manual_account_disconnect_track_event( 'yes' === $settings['testmode'] );
270274
} else {
271275
$this->record_manual_account_key_update_track_event( 'yes' === $settings['testmode'] );

includes/connect/class-wc-stripe-connect-api.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,24 @@ public function get_stripe_oauth_keys( $code, $type = 'connect', $mode = 'live'
8888
return $this->request( 'POST', $path, $request );
8989
}
9090

91+
/**
92+
* Sends a request to the Connect Server for Stripe App refreshed keys.
93+
*
94+
* @since 8.6.0
95+
*
96+
* @param string $refresh_token Stripe App OAuth refresh token.
97+
* @param string $mode Optional. The mode to refresh keys for. 'live' or 'test'. Default is 'live'.
98+
*
99+
* @return array
100+
*/
101+
public function refresh_stripe_app_oauth_keys( $refresh_token, $mode = 'live' ) {
102+
$request = [
103+
'refreshToken' => $refresh_token,
104+
'mode' => $mode,
105+
];
106+
return $this->request( 'POST', '/stripe/app-oauth-keys-refresh', $request );
107+
}
108+
91109
/**
92110
* General OAuth request method.
93111
*

includes/connect/class-wc-stripe-connect.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class WC_Stripe_Connect {
2727
public function __construct( WC_Stripe_Connect_API $api ) {
2828
$this->api = $api;
2929

30+
// refresh the connection, triggered by Action Scheduler
31+
add_action( 'wc_stripe_refresh_connection', [ $this, 'refresh_connection' ] );
32+
3033
add_action( 'admin_init', [ $this, 'maybe_handle_redirect' ] );
3134
}
3235

@@ -166,6 +169,20 @@ private function save_stripe_keys( $result, $type = 'connect', $mode = 'live' )
166169

167170
update_option( self::SETTINGS_OPTION, $options );
168171

172+
// Similar to what we do for webhooks, we save some stats to help debug oauth problems.
173+
update_option( 'wc_stripe_' . $prefix . 'oauth_updated_at', time() );
174+
update_option( 'wc_stripe_' . $prefix . 'oauth_failed_attempts', 0 );
175+
update_option( 'wc_stripe_' . $prefix . 'oauth_last_failed_at', '' );
176+
177+
if ( 'app' === $type ) {
178+
// Stripe App OAuth access_tokens expire after 1 hour:
179+
// https://docs.stripe.com/stripe-apps/api-authentication/oauth#refresh-access-token
180+
$this->schedule_connection_refresh();
181+
} else {
182+
// Make sure that all refresh actions are cancelled if they haven't connected via the app.
183+
$this->unschedule_connection_refresh();
184+
}
185+
169186
try {
170187
// Automatically configure webhooks for the account now that we have the keys.
171188
WC_Stripe::get_instance()->account->configure_webhooks( $is_test ? 'test' : 'live', $secret_key );
@@ -247,6 +264,26 @@ public function is_connected_via_oauth( $mode = 'live' ) {
247264
return isset( $options[ $key ] ) && in_array( $options[ $key ], [ 'connect', 'app' ], true );
248265
}
249266

267+
/**
268+
* Determines if the store is using a Stripe App OAuth connection.
269+
*
270+
* @since 8.6.0
271+
*
272+
* @param string $mode Optional. The mode to check. 'live' | 'test' | null (default: null).
273+
* @return bool True if connected via Stripe App OAuth, false otherwise.
274+
*/
275+
public function is_connected_via_app_oauth( $mode = null ) {
276+
$options = get_option( self::SETTINGS_OPTION, [] );
277+
278+
// If the mode is not provided, we'll check the current mode.
279+
if ( is_null( $mode ) ) {
280+
$mode = isset( $options['testmode'] ) && 'yes' === $options['testmode'] ? 'test' : 'live';
281+
}
282+
$key = 'test' === $mode ? 'test_connection_type' : 'connection_type';
283+
284+
return isset( $options[ $key ] ) && 'app' === $options[ $key ];
285+
}
286+
250287
/**
251288
* Records a track event after the user is redirected back to the store from the Stripe UX.
252289
*
@@ -265,5 +302,83 @@ private function record_account_connect_track_event( bool $had_error ) {
265302
// a queue wouldn't be processed due to the redirect that comes after.
266303
WC_Tracks::record_event( $event_name, [ 'is_test_mode' => $is_test ] );
267304
}
305+
306+
/**
307+
* Schedules the App OAuth connection refresh.
308+
*
309+
* @since 8.6.0
310+
*/
311+
private function schedule_connection_refresh() {
312+
if ( ! $this->is_connected_via_app_oauth() ) {
313+
return;
314+
}
315+
316+
/**
317+
* Filters the frequency with which the App OAuth connection should be refreshed.
318+
* Access tokens expire in 1 hour, and there seem to be no way to customize that from the Stripe Dashboard:
319+
* https://docs.stripe.com/stripe-apps/api-authentication/oauth#refresh-access-token
320+
* We schedule the connection refresh every 55 minutues.
321+
*
322+
* @param int $interval refresh interval
323+
*
324+
* @since 8.6.0
325+
*/
326+
$interval = apply_filters( 'wc_stripe_connection_refresh_interval', HOUR_IN_SECONDS - 5 * MINUTE_IN_SECONDS );
327+
328+
// Make sure that all refresh actions are cancelled before scheduling it.
329+
$this->unschedule_connection_refresh();
330+
331+
as_schedule_single_action( time() + $interval, 'wc_stripe_refresh_connection', [], WC_Stripe_Action_Scheduler_Service::GROUP_ID, false, 0 );
332+
}
333+
334+
/**
335+
* Unschedules the App OAuth connection refresh.
336+
*
337+
* @since 8.6.0
338+
*/
339+
protected function unschedule_connection_refresh() {
340+
as_unschedule_all_actions( 'wc_stripe_refresh_connection', [], WC_Stripe_Action_Scheduler_Service::GROUP_ID );
341+
}
342+
343+
/**
344+
* Refreshes the App OAuth access_token via the Woo Connect Server.
345+
*
346+
* @since 8.6.0
347+
*/
348+
public function refresh_connection() {
349+
if ( ! $this->is_connected_via_app_oauth() ) {
350+
return;
351+
}
352+
353+
$options = get_option( self::SETTINGS_OPTION, [] );
354+
$mode = isset( $options['testmode'] ) && 'yes' === $options['testmode'] ? 'test' : 'live';
355+
$prefix = 'test' === $mode ? 'test_' : '';
356+
$refresh_token = $options[ $prefix . 'refresh_token' ];
357+
358+
$retries = get_option( 'wc_stripe_' . $prefix . 'oauth_failed_attempts', 0 ) + 1;
359+
360+
$response = $this->api->refresh_stripe_app_oauth_keys( $refresh_token, $mode );
361+
if ( ! is_wp_error( $response ) ) {
362+
$response = $this->save_stripe_keys( $response, 'app', $mode );
363+
}
364+
365+
if ( is_wp_error( $response ) ) {
366+
update_option( 'wc_stripe_' . $prefix . 'oauth_failed_attempts', $retries );
367+
update_option( 'wc_stripe_' . $prefix . 'oauth_last_failed_at', time() );
368+
369+
WC_Stripe_Logger::log( 'OAuth connection refresh failed: ' . print_r( $response, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
370+
371+
// If after 10 attempts we are unable to refresh the connection keys, we don't re-schedule anymore,
372+
// in this case an error message is show in the account status indicating that the API keys are not
373+
// valid and that a reconnection is necessary.
374+
if ( $retries < 10 ) {
375+
// Re-schedule the connection refresh
376+
$this->schedule_connection_refresh();
377+
}
378+
}
379+
380+
// save_stripe_keys() schedules a connection_refresh after saving the keys,
381+
// we don't need to do it explicitly here.
382+
}
268383
}
269384
}

0 commit comments

Comments
 (0)