@@ -27,6 +27,9 @@ class WC_Stripe_Connect {
27
27
public function __construct ( WC_Stripe_Connect_API $ api ) {
28
28
$ this ->api = $ api ;
29
29
30
+ // refresh the connection, triggered by Action Scheduler
31
+ add_action ( 'wc_stripe_refresh_connection ' , [ $ this , 'refresh_connection ' ] );
32
+
30
33
add_action ( 'admin_init ' , [ $ this , 'maybe_handle_redirect ' ] );
31
34
}
32
35
@@ -166,6 +169,20 @@ private function save_stripe_keys( $result, $type = 'connect', $mode = 'live' )
166
169
167
170
update_option ( self ::SETTINGS_OPTION , $ options );
168
171
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
+
169
186
try {
170
187
// Automatically configure webhooks for the account now that we have the keys.
171
188
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' ) {
247
264
return isset ( $ options [ $ key ] ) && in_array ( $ options [ $ key ], [ 'connect ' , 'app ' ], true );
248
265
}
249
266
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
+
250
287
/**
251
288
* Records a track event after the user is redirected back to the store from the Stripe UX.
252
289
*
@@ -265,5 +302,83 @@ private function record_account_connect_track_event( bool $had_error ) {
265
302
// a queue wouldn't be processed due to the redirect that comes after.
266
303
WC_Tracks::record_event ( $ event_name , [ 'is_test_mode ' => $ is_test ] );
267
304
}
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
+ }
268
383
}
269
384
}
0 commit comments