PluginProbe
WooCommerce / 11.1.0
WooCommerce v11.1.0
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / src / Internal / Admin / Settings / Payments.php
Payments.php
762 lines 29.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare( strict_types=1 );
3
4 namespace Automattic\WooCommerce\Internal\Admin\Settings;
5
6 use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsService;
7 use Automattic\WooCommerce\Internal\Admin\Suggestions\PaymentsExtensionSuggestions as ExtensionSuggestions;
8 use Automattic\WooCommerce\Internal\Logging\SafeGlobalFunctionProxy;
9 use Exception;
10
11 defined( 'ABSPATH' ) || exit;
12 /**
13 * Payments settings service class.
14 *
15 * @internal
16 */
17 class Payments {
18
19 const PAYMENTS_NOX_PROFILE_KEY = 'woocommerce_payments_nox_profile';
20 const PAYMENTS_PROVIDER_STATE_SNAPSHOTS_KEY = 'woocommerce_payments_provider_state_snapshots';
21
22 const SUGGESTIONS_CONTEXT = 'wc_settings_payments';
23
24 const EVENT_PREFIX = 'settings_payments_';
25
26 const FROM_PAYMENTS_SETTINGS = 'WCADMIN_PAYMENT_SETTINGS';
27 const FROM_PAYMENTS_MENU_ITEM = 'PAYMENTS_MENU_ITEM';
28 const FROM_PAYMENTS_TASK = 'WCADMIN_PAYMENT_TASK';
29 const FROM_ADDITIONAL_PAYMENTS_TASK = 'WCADMIN_ADDITIONAL_PAYMENT_TASK';
30 const FROM_PROVIDER_ONBOARDING = 'PROVIDER_ONBOARDING';
31
32 private const PROVIDERS_REQUEST_CACHE_GROUP = PaymentsProviders::PROVIDER_LISTS_REQUEST_CACHE_GROUP;
33 private const PROVIDERS_REQUEST_CACHE_KEY = PaymentsProviders::PROVIDER_LISTS_REQUEST_CACHE_KEY;
34
35 /**
36 * The payment providers service.
37 *
38 * @var PaymentsProviders
39 */
40 private PaymentsProviders $providers;
41
42 /**
43 * The payment extension suggestions service.
44 *
45 * @var ExtensionSuggestions
46 */
47 private ExtensionSuggestions $extension_suggestions;
48
49 /**
50 * Initialize the class instance.
51 *
52 * @param PaymentsProviders $payment_providers The payment providers service.
53 * @param ExtensionSuggestions $payment_extension_suggestions The payment extension suggestions service.
54 *
55 * @internal
56 */
57 final public function init( PaymentsProviders $payment_providers, ExtensionSuggestions $payment_extension_suggestions ): void {
58 $this->providers = $payment_providers;
59 $this->extension_suggestions = $payment_extension_suggestions;
60
61 wp_cache_add_non_persistent_groups( array( self::PROVIDERS_REQUEST_CACHE_GROUP ) );
62 }
63
64 /**
65 * Get the payment provider details list for the settings page.
66 *
67 * @param string $location The location for which the providers are being determined.
68 * This is an ISO 3166-1 alpha-2 country code.
69 * @param bool $for_display Optional. Whether the payment providers list is intended for display purposes or
70 * it is meant to be used for internal business logic.
71 * Primarily, this means that when it is not for display, we will use the raw
72 * payment gateways list (all the registered gateways), not just the ones that
73 * should be shown to the user on the Payments Settings page.
74 * This complication is for backward compatibility as it relates to legacy settings hooks
75 * being fired or not.
76 * @param bool $remove_shells Optional. Whether to remove the payment providers shells from the list.
77 * If the $for_display is true, this will be ignored since the display logic will
78 * handle the shells itself.
79 *
80 * @return array The payment providers details list.
81 * @throws Exception If there are malformed or invalid suggestions.
82 */
83 public function get_payment_providers( string $location, bool $for_display = true, bool $remove_shells = false ): array {
84 $can_install_plugins = current_user_can( 'install_plugins' );
85 $cache_key = get_current_user_id() . '__' . ( $can_install_plugins ? '1' : '0' ) . '__' . strtoupper( $location ) . '__' . ( $for_display ? '1' : '0' ) . ( $remove_shells ? '1' : '0' );
86 $cached_provider_lists = wp_cache_get( self::PROVIDERS_REQUEST_CACHE_KEY, self::PROVIDERS_REQUEST_CACHE_GROUP );
87 if ( is_array( $cached_provider_lists ) && isset( $cached_provider_lists[ $cache_key ] ) && is_array( $cached_provider_lists[ $cache_key ] ) ) {
88 return $cached_provider_lists[ $cache_key ];
89 }
90
91 $payment_gateways = $this->providers->get_payment_gateways( $for_display );
92 if ( ! $for_display && $remove_shells ) {
93 $payment_gateways = $this->providers->remove_shell_payment_gateways( $payment_gateways, $location );
94 }
95
96 $providers_order_map = $this->providers->get_order_map();
97
98 $payment_providers = array();
99
100 // Only include suggestions if the requesting user can install plugins.
101 $suggestions = array();
102 if ( $can_install_plugins ) {
103 $suggestions = $this->providers->get_extension_suggestions( $location, self::SUGGESTIONS_CONTEXT );
104 }
105 // If we have preferred suggestions, add them to the providers list.
106 if ( ! empty( $suggestions['preferred'] ) ) {
107 // Sort them by priority, ASC.
108 usort(
109 $suggestions['preferred'],
110 function ( $a, $b ) {
111 return $a['_priority'] <=> $b['_priority'];
112 }
113 );
114
115 // By default, we will add the preferred suggestions at the top of the list.
116 $last_preferred_order = -1;
117 // If WooPayments is already present, we add the preferred suggestions after it.
118 // This way we ensure default installed WooPayments is at the same place as its suggestion would be.
119 if ( isset( $providers_order_map[ WooPaymentsService::GATEWAY_ID ] ) ) {
120 $last_preferred_order = $providers_order_map[ WooPaymentsService::GATEWAY_ID ];
121 }
122
123 foreach ( $suggestions['preferred'] as $suggestion ) {
124 $suggestion_order_map_id = $this->providers->get_suggestion_order_map_id( $suggestion['id'] );
125 // Determine the suggestion's order value.
126 // If we don't have an order for it, add it to the top but keep the relative order:
127 // PSP first, APM after PSP, offline PSP after PSP and APM.
128 if ( ! isset( $providers_order_map[ $suggestion_order_map_id ] ) ) {
129 $providers_order_map = Utils::order_map_add_at_order( $providers_order_map, $suggestion_order_map_id, $last_preferred_order + 1 );
130 }
131
132 // Save the preferred provider's order to know where we should be inserting next.
133 // But only if the last preferred order is less than the current one.
134 if ( $last_preferred_order < $providers_order_map[ $suggestion_order_map_id ] ) {
135 $last_preferred_order = $providers_order_map[ $suggestion_order_map_id ];
136 }
137
138 // Change suggestion details to align it with a regular payment gateway.
139 $suggestion['_suggestion_id'] = $suggestion['id'];
140 $suggestion['id'] = $suggestion_order_map_id;
141 $suggestion['_type'] = PaymentsProviders::TYPE_SUGGESTION;
142 $suggestion['_order'] = $providers_order_map[ $suggestion_order_map_id ];
143 unset( $suggestion['_priority'] );
144
145 $payment_providers[] = $suggestion;
146 }
147 }
148
149 foreach ( $payment_gateways as $payment_gateway ) {
150 // Determine the gateway's order value.
151 // If we don't have an order for it, place it above offline PMs if the offline group
152 // is still at the bottom (default ordering). Otherwise, add to the end.
153 if ( ! isset( $providers_order_map[ $payment_gateway->id ] ) ) {
154 $providers_order_map = $this->providers->order_map_add_gateway( $providers_order_map, $payment_gateway->id );
155 }
156
157 $payment_providers[] = $this->providers->get_payment_gateway_details(
158 $payment_gateway,
159 $providers_order_map[ $payment_gateway->id ],
160 $location
161 );
162 }
163
164 // Add offline payment methods group entry if we have offline payment methods.
165 if ( in_array( PaymentsProviders::TYPE_OFFLINE_PM, array_column( $payment_providers, '_type' ), true ) ) {
166 // Determine the item's order value.
167 // If we don't have an order for it, add it to the end.
168 if ( ! isset( $providers_order_map[ PaymentsProviders::OFFLINE_METHODS_ORDERING_GROUP ] ) ) {
169 $providers_order_map = Utils::order_map_add_at_order( $providers_order_map, PaymentsProviders::OFFLINE_METHODS_ORDERING_GROUP, count( $payment_providers ) );
170 }
171
172 $payment_providers[] = array(
173 'id' => PaymentsProviders::OFFLINE_METHODS_ORDERING_GROUP,
174 '_type' => PaymentsProviders::TYPE_OFFLINE_PMS_GROUP,
175 '_order' => $providers_order_map[ PaymentsProviders::OFFLINE_METHODS_ORDERING_GROUP ],
176 'title' => esc_html__( 'Take offline payments', 'woocommerce' ),
177 'description' => esc_html__( 'Accept payments offline using multiple different methods. These can also be used to test purchases.', 'woocommerce' ),
178 'icon' => plugins_url( 'assets/images/payment_methods/cod.svg', WC_PLUGIN_FILE ),
179 // The offline PMs (and their group) are obviously from WooCommerce, and WC is always active.
180 'plugin' => array(
181 '_type' => 'wporg',
182 'slug' => 'woocommerce',
183 'file' => '', // This pseudo-provider should have no use for the plugin file.
184 'status' => PaymentsProviders::EXTENSION_ACTIVE,
185 ),
186 'management' => array(
187 '_links' => array(
188 'settings' => array(
189 'href' => Utils::wc_payments_settings_url( '/' . ( class_exists( '\WC_Settings_Payment_Gateways' ) ? \WC_Settings_Payment_Gateways::OFFLINE_SECTION_NAME : 'offline' ) ),
190 ),
191 ),
192 ),
193 );
194 }
195
196 // Determine the final, standardized providers order map.
197 $providers_order_map = $this->providers->enhance_order_map( $providers_order_map );
198 // Enforce the order map on all providers, just in case.
199 foreach ( $payment_providers as $key => $provider ) {
200 $payment_providers[ $key ]['_order'] = $providers_order_map[ $provider['id'] ];
201 }
202 // NOTE: For now, save it back to the DB. This is temporary until we have a better way to handle this!
203 $this->providers->save_order_map( $providers_order_map );
204
205 // Sort the payment providers by order, ASC.
206 usort(
207 $payment_providers,
208 function ( $a, $b ) {
209 return $a['_order'] <=> $b['_order'];
210 }
211 );
212
213 // Only process payment provider states if we are displaying the providers.
214 // This is to ensure we don't introduce any performance issues outside the Payments settings page.
215 if ( $for_display ) {
216 $this->process_payment_provider_states( $payment_providers );
217 }
218
219 if ( ! is_array( $cached_provider_lists ) ) {
220 $cached_provider_lists = array();
221 }
222 $cached_provider_lists[ $cache_key ] = $payment_providers;
223 wp_cache_set( self::PROVIDERS_REQUEST_CACHE_KEY, $cached_provider_lists, self::PROVIDERS_REQUEST_CACHE_GROUP );
224
225 return $payment_providers;
226 }
227
228 /**
229 * Get the payment extension suggestions for the given location.
230 *
231 * @param string $location The location for which the suggestions are being fetched.
232 *
233 * @return array[] The payment extension suggestions for the given location, split into preferred and other.
234 * @throws Exception If there are malformed or invalid suggestions.
235 */
236 public function get_payment_extension_suggestions( string $location ): array {
237 return $this->providers->get_extension_suggestions( $location, self::SUGGESTIONS_CONTEXT );
238 }
239
240 /**
241 * Get the payment extension suggestions categories details.
242 *
243 * @return array The payment extension suggestions categories.
244 */
245 public function get_payment_extension_suggestion_categories(): array {
246 return $this->providers->get_extension_suggestion_categories();
247 }
248
249 /**
250 * Get the business location country code for the Payments settings.
251 *
252 * @return string The ISO 3166-1 alpha-2 country code to use for the overall business location.
253 * If the user didn't set a location, the WC base location country code is used.
254 */
255 public function get_country(): string {
256 $user_nox_meta = get_user_meta( get_current_user_id(), self::PAYMENTS_NOX_PROFILE_KEY, true );
257 if ( ! empty( $user_nox_meta['business_country_code'] ) ) {
258 return $user_nox_meta['business_country_code'];
259 }
260
261 return WC()->countries->get_base_country();
262 }
263
264 /**
265 * Set the business location country for the Payments settings.
266 *
267 * @param string $location The country code. This should be an ISO 3166-1 alpha-2 country code.
268 */
269 public function set_country( string $location ): bool {
270 $previous_country = $this->get_country();
271
272 $user_payments_nox_profile = get_user_meta( get_current_user_id(), self::PAYMENTS_NOX_PROFILE_KEY, true );
273
274 if ( empty( $user_payments_nox_profile ) ) {
275 $user_payments_nox_profile = array();
276 } else {
277 $user_payments_nox_profile = maybe_unserialize( $user_payments_nox_profile );
278 }
279 $user_payments_nox_profile['business_country_code'] = $location;
280
281 $result = false !== update_user_meta( get_current_user_id(), self::PAYMENTS_NOX_PROFILE_KEY, $user_payments_nox_profile );
282
283 if ( $result && $previous_country !== $location ) {
284 // Record an event that the business location (registration country code) was changed.
285 $this->record_event(
286 'business_location_update',
287 array(
288 'business_country' => $location,
289 'previous_business_country' => $previous_country,
290 )
291 );
292 }
293
294 return $result;
295 }
296
297 /**
298 * Update the payment providers order map.
299 *
300 * @param array $order_map The new order for payment providers.
301 *
302 * @return bool True if the payment providers ordering was successfully updated, false otherwise.
303 */
304 public function update_payment_providers_order_map( array $order_map ): bool {
305 $result = $this->providers->update_payment_providers_order_map( $order_map );
306
307 if ( $result ) {
308 // The order map influences the providers list, so clear the cached data.
309 $this->clear_cache();
310
311 // Record an event that the payment providers order map was updated.
312 $this->record_event(
313 'payment_providers_order_map_updated',
314 array(
315 'order_map' => implode( ', ', array_keys( $this->providers->get_order_map() ) ),
316 )
317 );
318 }
319
320 return $result;
321 }
322
323 /**
324 * Attach a payment extension suggestion.
325 *
326 * This is only an internal recording of attachment. No actual extension installation or activation happens.
327 *
328 * @param string $id The ID of the payment extension suggestion to attach.
329 *
330 * @return bool True if the suggestion was successfully marked as attached, false otherwise.
331 * @throws Exception If the suggestion ID is invalid.
332 */
333 public function attach_payment_extension_suggestion( string $id ): bool {
334 $result = $this->providers->attach_extension_suggestion( $id );
335
336 if ( $result ) {
337 // The attachment influences the providers list, so clear the cached data.
338 $this->clear_cache();
339
340 // Record an event that the suggestion was attached.
341 $this->record_event(
342 'extension_suggestion_attached',
343 array(
344 'suggestion_id' => $id,
345 )
346 );
347 }
348
349 return $result;
350 }
351
352 /**
353 * Hide a payment extension suggestion.
354 *
355 * @param string $id The ID of the payment extension suggestion to hide.
356 *
357 * @return bool True if the suggestion was successfully hidden, false otherwise.
358 * @throws Exception If the suggestion ID is invalid.
359 */
360 public function hide_payment_extension_suggestion( string $id ): bool {
361 $result = $this->providers->hide_extension_suggestion( $id );
362
363 if ( $result ) {
364 // Hidden suggestions are excluded from the providers list, so clear the cached data.
365 $this->clear_cache();
366
367 // Record an event that the suggestion was hidden.
368 $this->record_event(
369 'extension_suggestion_hidden',
370 array(
371 'suggestion_id' => $id,
372 )
373 );
374 }
375
376 return $result;
377 }
378
379 /**
380 * Dismiss a payment extension suggestion incentive.
381 *
382 * @param string $suggestion_id The suggestion ID.
383 * @param string $incentive_id The incentive ID.
384 * @param string $context Optional. The context in which the incentive should be dismissed.
385 * Default is to dismiss the incentive in all contexts.
386 * @param bool $do_not_track Optional. If true, the incentive dismissal will not be tracked.
387 *
388 * @return bool True if the incentive was not previously dismissed and now it is.
389 * False if the incentive was already dismissed or could not be dismissed.
390 * @throws Exception If the incentive could not be dismissed due to an error.
391 */
392 public function dismiss_extension_suggestion_incentive( string $suggestion_id, string $incentive_id, string $context = 'all', bool $do_not_track = false ): bool {
393 $result = $this->extension_suggestions->dismiss_incentive( $incentive_id, $suggestion_id, $context );
394
395 if ( $result ) {
396 // Incentives are embedded in the providers list details, so clear the cached data.
397 $this->clear_cache();
398 }
399
400 if ( ! $do_not_track && $result ) {
401 // Record an event that the incentive was dismissed.
402 $this->record_event(
403 'incentive_dismiss',
404 array(
405 'suggestion_id' => $suggestion_id,
406 'incentive_id' => $incentive_id,
407 'display_context' => $context,
408 )
409 );
410 }
411
412 return $result;
413 }
414
415 /**
416 * Clear cached payment provider data.
417 *
418 * Call after changing provider ordering, suggestions, incentives, gateway registration,
419 * settings, or account state during a request. Also useful for testing purposes.
420 *
421 * @since 11.1.0
422 *
423 * @internal
424 * @return void
425 */
426 public function clear_cache(): void {
427 wp_cache_delete( self::PROVIDERS_REQUEST_CACHE_KEY, self::PROVIDERS_REQUEST_CACHE_GROUP );
428 $this->providers->clear_cache();
429 }
430
431 /**
432 * Send a Tracks event.
433 *
434 * By default, Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version`
435 * properties to every event.
436 *
437 * @param string $name The event name.
438 * If it is not prefixed with self::EVENT_PREFIX, it will be prefixed with it.
439 * @param array $properties Optional. The event custom properties.
440 * These properties will be merged with the default properties.
441 * Default properties values take precedence over the provided ones.
442 *
443 * @return void
444 */
445 private function record_event( string $name, array $properties = array() ) {
446 if ( ! function_exists( 'wc_admin_record_tracks_event' ) ) {
447 return;
448 }
449
450 // If the event name is empty, we don't record it.
451 if ( empty( $name ) ) {
452 return;
453 }
454
455 // If the event name is not prefixed with `settings_payments_`, we prefix it.
456 if ( ! str_starts_with( $name, self::EVENT_PREFIX ) ) {
457 $name = self::EVENT_PREFIX . $name;
458 }
459
460 // Add default properties to every event and overwrite custom properties with the same keys.
461 $properties = array_merge(
462 $properties,
463 array(
464 'business_country' => $this->get_country(),
465 ),
466 );
467
468 wc_admin_record_tracks_event( $name, $properties );
469 }
470
471 /**
472 * Process the payment providers states and update the snapshots in the DB.
473 *
474 * @param array $payment_providers The payment providers details list.
475 */
476 private function process_payment_provider_states( array $payment_providers ): void {
477 // Read the current state snapshots from the DB.
478 $snapshots = get_option( self::PAYMENTS_PROVIDER_STATE_SNAPSHOTS_KEY, array() );
479 if ( ! is_array( $snapshots ) ) {
480 $snapshots = array();
481 }
482
483 $default_snapshot = array(
484 'extension_active' => false,
485 'account_connected' => false,
486 'account_test_mode' => false,
487 'needs_setup' => false,
488 'test_mode' => false,
489 );
490
491 // Iterate through the payment providers and generate their updated snapshots.
492 // We will use the provider's plugin slug as the key for the snapshot to ensure uniqueness.
493 // For now, we will only focus on the provider state for official extensions, not all the gateways.
494 $new_snapshots = array();
495 foreach ( $payment_providers as $provider ) {
496 if ( empty( $provider['plugin']['slug'] ) ||
497 empty( $provider['id'] ) ||
498 empty( $provider['state'] ) || ! is_array( $provider['state'] ) ||
499 empty( $provider['onboarding']['state'] ) || ! is_array( $provider['onboarding']['state'] ) ||
500 empty( $provider['_type'] ) ||
501 PaymentsProviders::TYPE_GATEWAY !== $provider['_type'] ||
502 empty( $provider['_suggestion_id'] )
503 ) {
504 continue;
505 }
506
507 $snapshot_key = $provider['plugin']['slug'];
508
509 // Since we are going after the provider general state, not that of the specific gateway,
510 // we only need to look at the first found gateway from a given provider.
511 if ( isset( $new_snapshots[ $snapshot_key ] ) ) {
512 continue;
513 }
514
515 // If we don't have an already existing snapshot for this provider, we create one with default values.
516 // This way we can track changes even for the first time we see a provider.
517 if ( ! isset( $snapshots[ $snapshot_key ] ) ) {
518 $snapshots[ $snapshot_key ] = $default_snapshot;
519 } else {
520 // Make sure the old snapshot has the same keys as the default one.
521 $snapshots[ $snapshot_key ] = array_merge( $default_snapshot, $snapshots[ $snapshot_key ] );
522 // Remove any keys that are not in the default snapshot.
523 $snapshot_keys = array_keys( $default_snapshot );
524 foreach ( $snapshots[ $snapshot_key ] as $key => $v ) {
525 if ( ! in_array( $key, $snapshot_keys, true ) ) {
526 unset( $snapshots[ $snapshot_key ][ $key ] );
527 }
528 }
529
530 // Always sort the old snapshot by keys to ensure consistency.
531 ksort( $snapshots[ $snapshot_key ] );
532 }
533
534 // Generate the new snapshot for the provider.
535 $new_snapshots[ $snapshot_key ] = array(
536 'extension_active' => true, // The extension is definitely active since we have a gateway from it.
537 'account_connected' => $provider['state']['account_connected'] ?? $default_snapshot['account_connected'],
538 'account_test_mode' => $provider['onboarding']['state']['test_mode'] ?? $default_snapshot['account_test_mode'],
539 'needs_setup' => $provider['state']['needs_setup'] ?? $default_snapshot['needs_setup'],
540 'test_mode' => $provider['state']['test_mode'] ?? $default_snapshot['test_mode'],
541 );
542
543 // Always sort the new snapshot by keys to ensure consistency.
544 ksort( $new_snapshots[ $snapshot_key ] );
545 }
546
547 // Provider snapshots that are not in the new snapshots but were in the old ones should be kept but marked as inactive.
548 foreach ( $snapshots as $snapshot_key => $old_snapshot ) {
549 if ( ! isset( $new_snapshots[ $snapshot_key ] ) ) {
550 $new_snapshots[ $snapshot_key ] = $old_snapshot;
551 $new_snapshots[ $snapshot_key ]['extension_active'] = false;
552 }
553 }
554
555 // Always order the new snapshots by keys to ensure DB updates happen only when the data changes.
556 ksort( $new_snapshots );
557
558 // Save the new snapshots back to the DB, as soon as we have them ready to avoid concurrent state change tracking.
559 // No need to autoload this option since it will be used only in the Payments Settings area.
560 $result = update_option( self::PAYMENTS_PROVIDER_STATE_SNAPSHOTS_KEY, $new_snapshots, false );
561 if ( ! $result ) {
562 // If we didn't update the option, we don't need to track any changes.
563 return;
564 }
565
566 try {
567 $this->maybe_track_providers_state_change( $payment_providers, $snapshots, $new_snapshots );
568 } catch ( \Throwable $exception ) {
569 // If we failed to track the changes, we log the error but don't throw it.
570 // This is to avoid breaking the Payments Settings page.
571 SafeGlobalFunctionProxy::wc_get_logger()->error(
572 'Failed to track payment providers state change: ' . $exception->getMessage(),
573 array(
574 'source' => 'settings-payments',
575 )
576 );
577 }
578 }
579
580 /**
581 * Maybe track the payment providers state change.
582 *
583 * This method will iterate through the new snapshots and compare them with the old ones.
584 * If there are any changes, it will track them.
585 *
586 * @param array $providers The list of payment provider details.
587 * @param array $old_snapshots The old snapshots of the providers' states.
588 * @param array $new_snapshots The new snapshots of the providers' states.
589 */
590 private function maybe_track_providers_state_change( array $providers, array $old_snapshots, array $new_snapshots ): void {
591 foreach ( $new_snapshots as $provider_extension_slug => $new_snapshot ) {
592 if ( ! isset( $old_snapshots[ $provider_extension_slug ] ) ) {
593 // If we don't have an old snapshot for this provider, we can't track the change.
594 continue;
595 }
596
597 // If there are no changes, we don't need to track anything.
598 if ( maybe_serialize( $old_snapshots[ $provider_extension_slug ] ) === maybe_serialize( $new_snapshot ) ) {
599 continue;
600 }
601
602 // Search for the provider by its plugin slug.
603 $provider = null;
604 foreach ( $providers as $p ) {
605 if ( isset( $p['plugin']['slug'] ) && $p['plugin']['slug'] === $provider_extension_slug ) {
606 $provider = $p;
607 break;
608 }
609 }
610 if ( ! $provider ) {
611 // If we couldn't find the provider in the list it means the extension was deactivated.
612 // Get the matching suggestion by its slug.
613 $provider = $this->providers->get_extension_suggestion_by_plugin_slug( $provider_extension_slug );
614 if ( ! empty( $provider['id'] ) ) {
615 // If we found the suggestion, we can use it as a replacement provider.
616 // We need to set the `_suggestion_id` so we can handle the date more uniformly.
617 $provider['_suggestion_id'] = $provider['id'];
618 }
619 }
620 if ( ! $provider ) {
621 continue;
622 }
623
624 $this->maybe_track_provider_state_change( $provider, $old_snapshots[ $provider_extension_slug ], $new_snapshot );
625 }
626 }
627
628 /**
629 * Track the payment provider state change.
630 *
631 * @param array $provider The payment provider details.
632 * @param array $old_snapshot The old snapshot of the provider's state.
633 * @param array $new_snapshot The new snapshot of the provider's state.
634 */
635 private function maybe_track_provider_state_change( array $provider, array $old_snapshot, array $new_snapshot ): void {
636 // Note: Keep the order of the events in a way that makes sense for the onboarding flow.
637
638 // Track extension_active change.
639 if ( $old_snapshot['extension_active'] && ! $new_snapshot['extension_active'] ) {
640 $this->record_event(
641 'provider_extension_deactivated',
642 array(
643 'provider_id' => $provider['id'],
644 'suggestion_id' => $provider['_suggestion_id'],
645 'provider_extension_slug' => $provider['plugin']['slug'],
646 )
647 );
648
649 // If the extension was also uninstalled, we can track that as well.
650 if ( ! empty( $provider['plugin']['status'] ) && PaymentsProviders::EXTENSION_NOT_INSTALLED === $provider['plugin']['status'] ) {
651 $this->record_event(
652 'provider_extension_uninstalled',
653 array(
654 'provider_id' => $provider['id'],
655 'suggestion_id' => $provider['_suggestion_id'],
656 'provider_extension_slug' => $provider['plugin']['slug'],
657 )
658 );
659 }
660 } elseif ( ! $old_snapshot['extension_active'] && $new_snapshot['extension_active'] ) {
661 $this->record_event(
662 'provider_extension_activated',
663 array(
664 'provider_id' => $provider['id'],
665 'suggestion_id' => $provider['_suggestion_id'],
666 'provider_extension_slug' => $provider['plugin']['slug'],
667 )
668 );
669 }
670
671 // Track account_connected change.
672 if ( $old_snapshot['account_connected'] && ! $new_snapshot['account_connected'] ) {
673 $this->record_event(
674 'provider_account_disconnected',
675 array(
676 'provider_id' => $provider['id'],
677 'suggestion_id' => $provider['_suggestion_id'],
678 'provider_extension_slug' => $provider['plugin']['slug'],
679 'provider_account_test_mode' => $old_snapshot['account_test_mode'] ? 'yes' : 'no',
680 )
681 );
682 } elseif ( ! $old_snapshot['account_connected'] && $new_snapshot['account_connected'] ) {
683 $this->record_event(
684 'provider_account_connected',
685 array(
686 'provider_id' => $provider['id'],
687 'suggestion_id' => $provider['_suggestion_id'],
688 'provider_extension_slug' => $provider['plugin']['slug'],
689 'provider_account_test_mode' => $new_snapshot['account_test_mode'] ? 'yes' : 'no',
690 )
691 );
692 }
693
694 // Track needs_setup change.
695 if ( $old_snapshot['needs_setup'] && ! $new_snapshot['needs_setup'] ) {
696 $this->record_event(
697 'provider_setup_completed',
698 array(
699 'provider_id' => $provider['id'],
700 'suggestion_id' => $provider['_suggestion_id'],
701 'provider_extension_slug' => $provider['plugin']['slug'],
702 )
703 );
704 } elseif ( ! $old_snapshot['needs_setup'] && $new_snapshot['needs_setup'] ) {
705 $this->record_event(
706 'provider_setup_required',
707 array(
708 'provider_id' => $provider['id'],
709 'suggestion_id' => $provider['_suggestion_id'],
710 'provider_extension_slug' => $provider['plugin']['slug'],
711 )
712 );
713 }
714
715 // Track payments test_mode change, but only if an account is connected.
716 if ( $new_snapshot['account_connected'] ) {
717 if ( $old_snapshot['test_mode'] && ! $new_snapshot['test_mode'] ) {
718 $this->record_event(
719 'provider_live_payments_enabled',
720 array(
721 'provider_id' => $provider['id'],
722 'suggestion_id' => $provider['_suggestion_id'],
723 'provider_extension_slug' => $provider['plugin']['slug'],
724 )
725 );
726 } elseif ( ! $old_snapshot['test_mode'] && $new_snapshot['test_mode'] ) {
727 $this->record_event(
728 'provider_test_payments_enabled',
729 array(
730 'provider_id' => $provider['id'],
731 'suggestion_id' => $provider['_suggestion_id'],
732 'provider_extension_slug' => $provider['plugin']['slug'],
733 )
734 );
735 }
736 }
737
738 // Track account_test_mode change, but only if the account is connected.
739 if ( $new_snapshot['account_connected'] ) {
740 if ( $old_snapshot['account_test_mode'] && ! $new_snapshot['account_test_mode'] ) {
741 $this->record_event(
742 'provider_account_live_mode_enabled',
743 array(
744 'provider_id' => $provider['id'],
745 'suggestion_id' => $provider['_suggestion_id'],
746 'provider_extension_slug' => $provider['plugin']['slug'],
747 )
748 );
749 } elseif ( ! $old_snapshot['account_test_mode'] && $new_snapshot['account_test_mode'] ) {
750 $this->record_event(
751 'provider_account_test_mode_enabled',
752 array(
753 'provider_id' => $provider['id'],
754 'suggestion_id' => $provider['_suggestion_id'],
755 'provider_extension_slug' => $provider['plugin']['slug'],
756 )
757 );
758 }
759 }
760 }
761 }
762