PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.1
WooCommerce v10.9.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 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / Admin / Settings / Payments.php
woocommerce / src / Internal / Admin / Settings Last commit date
Exceptions 1 year ago PaymentsProviders 3 months ago SettingsUIPages 4 weeks ago LegacySettingsPageAdapter.php 4 weeks ago Payments.php 3 months ago PaymentsController.php 11 months ago PaymentsProviders.php 2 months ago PaymentsRestController.php 3 months ago RegisteredSettingsSectionAdapter.php 2 weeks ago SettingsUIPageInterface.php 4 weeks ago SettingsUIRequestContext.php 2 weeks ago SettingsUISchema.php 2 weeks ago Utils.php 2 months ago
Payments.php
714 lines
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 /**
33 * The payment providers service.
34 *
35 * @var PaymentsProviders
36 */
37 private PaymentsProviders $providers;
38
39 /**
40 * The payment extension suggestions service.
41 *
42 * @var ExtensionSuggestions
43 */
44 private ExtensionSuggestions $extension_suggestions;
45
46 /**
47 * Initialize the class instance.
48 *
49 * @param PaymentsProviders $payment_providers The payment providers service.
50 * @param ExtensionSuggestions $payment_extension_suggestions The payment extension suggestions service.
51 *
52 * @internal
53 */
54 final public function init( PaymentsProviders $payment_providers, ExtensionSuggestions $payment_extension_suggestions ): void {
55 $this->providers = $payment_providers;
56 $this->extension_suggestions = $payment_extension_suggestions;
57 }
58
59 /**
60 * Get the payment provider details list for the settings page.
61 *
62 * @param string $location The location for which the providers are being determined.
63 * This is an ISO 3166-1 alpha-2 country code.
64 * @param bool $for_display Optional. Whether the payment providers list is intended for display purposes or
65 * it is meant to be used for internal business logic.
66 * Primarily, this means that when it is not for display, we will use the raw
67 * payment gateways list (all the registered gateways), not just the ones that
68 * should be shown to the user on the Payments Settings page.
69 * This complication is for backward compatibility as it relates to legacy settings hooks
70 * being fired or not.
71 * @param bool $remove_shells Optional. Whether to remove the payment providers shells from the list.
72 * If the $for_display is true, this will be ignored since the display logic will
73 * handle the shells itself.
74 *
75 * @return array The payment providers details list.
76 * @throws Exception If there are malformed or invalid suggestions.
77 */
78 public function get_payment_providers( string $location, bool $for_display = true, bool $remove_shells = false ): array {
79 $payment_gateways = $this->providers->get_payment_gateways( $for_display );
80 if ( ! $for_display && $remove_shells ) {
81 $payment_gateways = $this->providers->remove_shell_payment_gateways( $payment_gateways, $location );
82 }
83
84 $providers_order_map = $this->providers->get_order_map();
85
86 $payment_providers = array();
87
88 // Only include suggestions if the requesting user can install plugins.
89 $suggestions = array();
90 if ( current_user_can( 'install_plugins' ) ) {
91 $suggestions = $this->providers->get_extension_suggestions( $location, self::SUGGESTIONS_CONTEXT );
92 }
93 // If we have preferred suggestions, add them to the providers list.
94 if ( ! empty( $suggestions['preferred'] ) ) {
95 // Sort them by priority, ASC.
96 usort(
97 $suggestions['preferred'],
98 function ( $a, $b ) {
99 return $a['_priority'] <=> $b['_priority'];
100 }
101 );
102
103 // By default, we will add the preferred suggestions at the top of the list.
104 $last_preferred_order = -1;
105 // If WooPayments is already present, we add the preferred suggestions after it.
106 // This way we ensure default installed WooPayments is at the same place as its suggestion would be.
107 if ( isset( $providers_order_map[ WooPaymentsService::GATEWAY_ID ] ) ) {
108 $last_preferred_order = $providers_order_map[ WooPaymentsService::GATEWAY_ID ];
109 }
110
111 foreach ( $suggestions['preferred'] as $suggestion ) {
112 $suggestion_order_map_id = $this->providers->get_suggestion_order_map_id( $suggestion['id'] );
113 // Determine the suggestion's order value.
114 // If we don't have an order for it, add it to the top but keep the relative order:
115 // PSP first, APM after PSP, offline PSP after PSP and APM.
116 if ( ! isset( $providers_order_map[ $suggestion_order_map_id ] ) ) {
117 $providers_order_map = Utils::order_map_add_at_order( $providers_order_map, $suggestion_order_map_id, $last_preferred_order + 1 );
118 }
119
120 // Save the preferred provider's order to know where we should be inserting next.
121 // But only if the last preferred order is less than the current one.
122 if ( $last_preferred_order < $providers_order_map[ $suggestion_order_map_id ] ) {
123 $last_preferred_order = $providers_order_map[ $suggestion_order_map_id ];
124 }
125
126 // Change suggestion details to align it with a regular payment gateway.
127 $suggestion['_suggestion_id'] = $suggestion['id'];
128 $suggestion['id'] = $suggestion_order_map_id;
129 $suggestion['_type'] = PaymentsProviders::TYPE_SUGGESTION;
130 $suggestion['_order'] = $providers_order_map[ $suggestion_order_map_id ];
131 unset( $suggestion['_priority'] );
132
133 $payment_providers[] = $suggestion;
134 }
135 }
136
137 foreach ( $payment_gateways as $payment_gateway ) {
138 // Determine the gateway's order value.
139 // If we don't have an order for it, place it above offline PMs if the offline group
140 // is still at the bottom (default ordering). Otherwise, add to the end.
141 if ( ! isset( $providers_order_map[ $payment_gateway->id ] ) ) {
142 $providers_order_map = $this->providers->order_map_add_gateway( $providers_order_map, $payment_gateway->id );
143 }
144
145 $payment_providers[] = $this->providers->get_payment_gateway_details(
146 $payment_gateway,
147 $providers_order_map[ $payment_gateway->id ],
148 $location
149 );
150 }
151
152 // Add offline payment methods group entry if we have offline payment methods.
153 if ( in_array( PaymentsProviders::TYPE_OFFLINE_PM, array_column( $payment_providers, '_type' ), true ) ) {
154 // Determine the item's order value.
155 // If we don't have an order for it, add it to the end.
156 if ( ! isset( $providers_order_map[ PaymentsProviders::OFFLINE_METHODS_ORDERING_GROUP ] ) ) {
157 $providers_order_map = Utils::order_map_add_at_order( $providers_order_map, PaymentsProviders::OFFLINE_METHODS_ORDERING_GROUP, count( $payment_providers ) );
158 }
159
160 $payment_providers[] = array(
161 'id' => PaymentsProviders::OFFLINE_METHODS_ORDERING_GROUP,
162 '_type' => PaymentsProviders::TYPE_OFFLINE_PMS_GROUP,
163 '_order' => $providers_order_map[ PaymentsProviders::OFFLINE_METHODS_ORDERING_GROUP ],
164 'title' => esc_html__( 'Take offline payments', 'woocommerce' ),
165 'description' => esc_html__( 'Accept payments offline using multiple different methods. These can also be used to test purchases.', 'woocommerce' ),
166 'icon' => plugins_url( 'assets/images/payment_methods/cod.svg', WC_PLUGIN_FILE ),
167 // The offline PMs (and their group) are obviously from WooCommerce, and WC is always active.
168 'plugin' => array(
169 '_type' => 'wporg',
170 'slug' => 'woocommerce',
171 'file' => '', // This pseudo-provider should have no use for the plugin file.
172 'status' => PaymentsProviders::EXTENSION_ACTIVE,
173 ),
174 'management' => array(
175 '_links' => array(
176 'settings' => array(
177 'href' => Utils::wc_payments_settings_url( '/' . ( class_exists( '\WC_Settings_Payment_Gateways' ) ? \WC_Settings_Payment_Gateways::OFFLINE_SECTION_NAME : 'offline' ) ),
178 ),
179 ),
180 ),
181 );
182 }
183
184 // Determine the final, standardized providers order map.
185 $providers_order_map = $this->providers->enhance_order_map( $providers_order_map );
186 // Enforce the order map on all providers, just in case.
187 foreach ( $payment_providers as $key => $provider ) {
188 $payment_providers[ $key ]['_order'] = $providers_order_map[ $provider['id'] ];
189 }
190 // NOTE: For now, save it back to the DB. This is temporary until we have a better way to handle this!
191 $this->providers->save_order_map( $providers_order_map );
192
193 // Sort the payment providers by order, ASC.
194 usort(
195 $payment_providers,
196 function ( $a, $b ) {
197 return $a['_order'] <=> $b['_order'];
198 }
199 );
200
201 // Only process payment provider states if we are displaying the providers.
202 // This is to ensure we don't introduce any performance issues outside the Payments settings page.
203 if ( $for_display ) {
204 $this->process_payment_provider_states( $payment_providers );
205 }
206
207 return $payment_providers;
208 }
209
210 /**
211 * Get the payment extension suggestions for the given location.
212 *
213 * @param string $location The location for which the suggestions are being fetched.
214 *
215 * @return array[] The payment extension suggestions for the given location, split into preferred and other.
216 * @throws Exception If there are malformed or invalid suggestions.
217 */
218 public function get_payment_extension_suggestions( string $location ): array {
219 return $this->providers->get_extension_suggestions( $location, self::SUGGESTIONS_CONTEXT );
220 }
221
222 /**
223 * Get the payment extension suggestions categories details.
224 *
225 * @return array The payment extension suggestions categories.
226 */
227 public function get_payment_extension_suggestion_categories(): array {
228 return $this->providers->get_extension_suggestion_categories();
229 }
230
231 /**
232 * Get the business location country code for the Payments settings.
233 *
234 * @return string The ISO 3166-1 alpha-2 country code to use for the overall business location.
235 * If the user didn't set a location, the WC base location country code is used.
236 */
237 public function get_country(): string {
238 $user_nox_meta = get_user_meta( get_current_user_id(), self::PAYMENTS_NOX_PROFILE_KEY, true );
239 if ( ! empty( $user_nox_meta['business_country_code'] ) ) {
240 return $user_nox_meta['business_country_code'];
241 }
242
243 return WC()->countries->get_base_country();
244 }
245
246 /**
247 * Set the business location country for the Payments settings.
248 *
249 * @param string $location The country code. This should be an ISO 3166-1 alpha-2 country code.
250 */
251 public function set_country( string $location ): bool {
252 $previous_country = $this->get_country();
253
254 $user_payments_nox_profile = get_user_meta( get_current_user_id(), self::PAYMENTS_NOX_PROFILE_KEY, true );
255
256 if ( empty( $user_payments_nox_profile ) ) {
257 $user_payments_nox_profile = array();
258 } else {
259 $user_payments_nox_profile = maybe_unserialize( $user_payments_nox_profile );
260 }
261 $user_payments_nox_profile['business_country_code'] = $location;
262
263 $result = false !== update_user_meta( get_current_user_id(), self::PAYMENTS_NOX_PROFILE_KEY, $user_payments_nox_profile );
264
265 if ( $result && $previous_country !== $location ) {
266 // Record an event that the business location (registration country code) was changed.
267 $this->record_event(
268 'business_location_update',
269 array(
270 'business_country' => $location,
271 'previous_business_country' => $previous_country,
272 )
273 );
274 }
275
276 return $result;
277 }
278
279 /**
280 * Update the payment providers order map.
281 *
282 * @param array $order_map The new order for payment providers.
283 *
284 * @return bool True if the payment providers ordering was successfully updated, false otherwise.
285 */
286 public function update_payment_providers_order_map( array $order_map ): bool {
287 $result = $this->providers->update_payment_providers_order_map( $order_map );
288
289 if ( $result ) {
290 // Record an event that the payment providers order map was updated.
291 $this->record_event(
292 'payment_providers_order_map_updated',
293 array(
294 'order_map' => implode( ', ', array_keys( $this->providers->get_order_map() ) ),
295 )
296 );
297 }
298
299 return $result;
300 }
301
302 /**
303 * Attach a payment extension suggestion.
304 *
305 * This is only an internal recording of attachment. No actual extension installation or activation happens.
306 *
307 * @param string $id The ID of the payment extension suggestion to attach.
308 *
309 * @return bool True if the suggestion was successfully marked as attached, false otherwise.
310 * @throws Exception If the suggestion ID is invalid.
311 */
312 public function attach_payment_extension_suggestion( string $id ): bool {
313 $result = $this->providers->attach_extension_suggestion( $id );
314
315 if ( $result ) {
316 // Record an event that the suggestion was attached.
317 $this->record_event(
318 'extension_suggestion_attached',
319 array(
320 'suggestion_id' => $id,
321 )
322 );
323 }
324
325 return $result;
326 }
327
328 /**
329 * Hide a payment extension suggestion.
330 *
331 * @param string $id The ID of the payment extension suggestion to hide.
332 *
333 * @return bool True if the suggestion was successfully hidden, false otherwise.
334 * @throws Exception If the suggestion ID is invalid.
335 */
336 public function hide_payment_extension_suggestion( string $id ): bool {
337 $result = $this->providers->hide_extension_suggestion( $id );
338
339 if ( $result ) {
340 // Record an event that the suggestion was hidden.
341 $this->record_event(
342 'extension_suggestion_hidden',
343 array(
344 'suggestion_id' => $id,
345 )
346 );
347 }
348
349 return $result;
350 }
351
352 /**
353 * Dismiss a payment extension suggestion incentive.
354 *
355 * @param string $suggestion_id The suggestion ID.
356 * @param string $incentive_id The incentive ID.
357 * @param string $context Optional. The context in which the incentive should be dismissed.
358 * Default is to dismiss the incentive in all contexts.
359 * @param bool $do_not_track Optional. If true, the incentive dismissal will not be tracked.
360 *
361 * @return bool True if the incentive was not previously dismissed and now it is.
362 * False if the incentive was already dismissed or could not be dismissed.
363 * @throws Exception If the incentive could not be dismissed due to an error.
364 */
365 public function dismiss_extension_suggestion_incentive( string $suggestion_id, string $incentive_id, string $context = 'all', bool $do_not_track = false ): bool {
366 $result = $this->extension_suggestions->dismiss_incentive( $incentive_id, $suggestion_id, $context );
367
368 if ( ! $do_not_track && $result ) {
369 // Record an event that the incentive was dismissed.
370 $this->record_event(
371 'incentive_dismiss',
372 array(
373 'suggestion_id' => $suggestion_id,
374 'incentive_id' => $incentive_id,
375 'display_context' => $context,
376 )
377 );
378 }
379
380 return $result;
381 }
382
383 /**
384 * Send a Tracks event.
385 *
386 * By default, Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version`
387 * properties to every event.
388 *
389 * @param string $name The event name.
390 * If it is not prefixed with self::EVENT_PREFIX, it will be prefixed with it.
391 * @param array $properties Optional. The event custom properties.
392 * These properties will be merged with the default properties.
393 * Default properties values take precedence over the provided ones.
394 *
395 * @return void
396 */
397 private function record_event( string $name, array $properties = array() ) {
398 if ( ! function_exists( 'wc_admin_record_tracks_event' ) ) {
399 return;
400 }
401
402 // If the event name is empty, we don't record it.
403 if ( empty( $name ) ) {
404 return;
405 }
406
407 // If the event name is not prefixed with `settings_payments_`, we prefix it.
408 if ( ! str_starts_with( $name, self::EVENT_PREFIX ) ) {
409 $name = self::EVENT_PREFIX . $name;
410 }
411
412 // Add default properties to every event and overwrite custom properties with the same keys.
413 $properties = array_merge(
414 $properties,
415 array(
416 'business_country' => $this->get_country(),
417 ),
418 );
419
420 wc_admin_record_tracks_event( $name, $properties );
421 }
422
423 /**
424 * Process the payment providers states and update the snapshots in the DB.
425 *
426 * @param array $payment_providers The payment providers details list.
427 */
428 private function process_payment_provider_states( array $payment_providers ): void {
429 // Read the current state snapshots from the DB.
430 $snapshots = get_option( self::PAYMENTS_PROVIDER_STATE_SNAPSHOTS_KEY, array() );
431 if ( ! is_array( $snapshots ) ) {
432 $snapshots = array();
433 }
434
435 $default_snapshot = array(
436 'extension_active' => false,
437 'account_connected' => false,
438 'account_test_mode' => false,
439 'needs_setup' => false,
440 'test_mode' => false,
441 );
442
443 // Iterate through the payment providers and generate their updated snapshots.
444 // We will use the provider's plugin slug as the key for the snapshot to ensure uniqueness.
445 // For now, we will only focus on the provider state for official extensions, not all the gateways.
446 $new_snapshots = array();
447 foreach ( $payment_providers as $provider ) {
448 if ( empty( $provider['plugin']['slug'] ) ||
449 empty( $provider['id'] ) ||
450 empty( $provider['state'] ) || ! is_array( $provider['state'] ) ||
451 empty( $provider['onboarding']['state'] ) || ! is_array( $provider['onboarding']['state'] ) ||
452 empty( $provider['_type'] ) ||
453 PaymentsProviders::TYPE_GATEWAY !== $provider['_type'] ||
454 empty( $provider['_suggestion_id'] )
455 ) {
456 continue;
457 }
458
459 $snapshot_key = $provider['plugin']['slug'];
460
461 // Since we are going after the provider general state, not that of the specific gateway,
462 // we only need to look at the first found gateway from a given provider.
463 if ( isset( $new_snapshots[ $snapshot_key ] ) ) {
464 continue;
465 }
466
467 // If we don't have an already existing snapshot for this provider, we create one with default values.
468 // This way we can track changes even for the first time we see a provider.
469 if ( ! isset( $snapshots[ $snapshot_key ] ) ) {
470 $snapshots[ $snapshot_key ] = $default_snapshot;
471 } else {
472 // Make sure the old snapshot has the same keys as the default one.
473 $snapshots[ $snapshot_key ] = array_merge( $default_snapshot, $snapshots[ $snapshot_key ] );
474 // Remove any keys that are not in the default snapshot.
475 $snapshot_keys = array_keys( $default_snapshot );
476 foreach ( $snapshots[ $snapshot_key ] as $key => $v ) {
477 if ( ! in_array( $key, $snapshot_keys, true ) ) {
478 unset( $snapshots[ $snapshot_key ][ $key ] );
479 }
480 }
481
482 // Always sort the old snapshot by keys to ensure consistency.
483 ksort( $snapshots[ $snapshot_key ] );
484 }
485
486 // Generate the new snapshot for the provider.
487 $new_snapshots[ $snapshot_key ] = array(
488 'extension_active' => true, // The extension is definitely active since we have a gateway from it.
489 'account_connected' => $provider['state']['account_connected'] ?? $default_snapshot['account_connected'],
490 'account_test_mode' => $provider['onboarding']['state']['test_mode'] ?? $default_snapshot['account_test_mode'],
491 'needs_setup' => $provider['state']['needs_setup'] ?? $default_snapshot['needs_setup'],
492 'test_mode' => $provider['state']['test_mode'] ?? $default_snapshot['test_mode'],
493 );
494
495 // Always sort the new snapshot by keys to ensure consistency.
496 ksort( $new_snapshots[ $snapshot_key ] );
497 }
498
499 // Provider snapshots that are not in the new snapshots but were in the old ones should be kept but marked as inactive.
500 foreach ( $snapshots as $snapshot_key => $old_snapshot ) {
501 if ( ! isset( $new_snapshots[ $snapshot_key ] ) ) {
502 $new_snapshots[ $snapshot_key ] = $old_snapshot;
503 $new_snapshots[ $snapshot_key ]['extension_active'] = false;
504 }
505 }
506
507 // Always order the new snapshots by keys to ensure DB updates happen only when the data changes.
508 ksort( $new_snapshots );
509
510 // Save the new snapshots back to the DB, as soon as we have them ready to avoid concurrent state change tracking.
511 // No need to autoload this option since it will be used only in the Payments Settings area.
512 $result = update_option( self::PAYMENTS_PROVIDER_STATE_SNAPSHOTS_KEY, $new_snapshots, false );
513 if ( ! $result ) {
514 // If we didn't update the option, we don't need to track any changes.
515 return;
516 }
517
518 try {
519 $this->maybe_track_providers_state_change( $payment_providers, $snapshots, $new_snapshots );
520 } catch ( \Throwable $exception ) {
521 // If we failed to track the changes, we log the error but don't throw it.
522 // This is to avoid breaking the Payments Settings page.
523 SafeGlobalFunctionProxy::wc_get_logger()->error(
524 'Failed to track payment providers state change: ' . $exception->getMessage(),
525 array(
526 'source' => 'settings-payments',
527 )
528 );
529 }
530 }
531
532 /**
533 * Maybe track the payment providers state change.
534 *
535 * This method will iterate through the new snapshots and compare them with the old ones.
536 * If there are any changes, it will track them.
537 *
538 * @param array $providers The list of payment provider details.
539 * @param array $old_snapshots The old snapshots of the providers' states.
540 * @param array $new_snapshots The new snapshots of the providers' states.
541 */
542 private function maybe_track_providers_state_change( array $providers, array $old_snapshots, array $new_snapshots ): void {
543 foreach ( $new_snapshots as $provider_extension_slug => $new_snapshot ) {
544 if ( ! isset( $old_snapshots[ $provider_extension_slug ] ) ) {
545 // If we don't have an old snapshot for this provider, we can't track the change.
546 continue;
547 }
548
549 // If there are no changes, we don't need to track anything.
550 if ( maybe_serialize( $old_snapshots[ $provider_extension_slug ] ) === maybe_serialize( $new_snapshot ) ) {
551 continue;
552 }
553
554 // Search for the provider by its plugin slug.
555 $provider = null;
556 foreach ( $providers as $p ) {
557 if ( isset( $p['plugin']['slug'] ) && $p['plugin']['slug'] === $provider_extension_slug ) {
558 $provider = $p;
559 break;
560 }
561 }
562 if ( ! $provider ) {
563 // If we couldn't find the provider in the list it means the extension was deactivated.
564 // Get the matching suggestion by its slug.
565 $provider = $this->providers->get_extension_suggestion_by_plugin_slug( $provider_extension_slug );
566 if ( ! empty( $provider['id'] ) ) {
567 // If we found the suggestion, we can use it as a replacement provider.
568 // We need to set the `_suggestion_id` so we can handle the date more uniformly.
569 $provider['_suggestion_id'] = $provider['id'];
570 }
571 }
572 if ( ! $provider ) {
573 continue;
574 }
575
576 $this->maybe_track_provider_state_change( $provider, $old_snapshots[ $provider_extension_slug ], $new_snapshot );
577 }
578 }
579
580 /**
581 * Track the payment provider state change.
582 *
583 * @param array $provider The payment provider details.
584 * @param array $old_snapshot The old snapshot of the provider's state.
585 * @param array $new_snapshot The new snapshot of the provider's state.
586 */
587 private function maybe_track_provider_state_change( array $provider, array $old_snapshot, array $new_snapshot ): void {
588 // Note: Keep the order of the events in a way that makes sense for the onboarding flow.
589
590 // Track extension_active change.
591 if ( $old_snapshot['extension_active'] && ! $new_snapshot['extension_active'] ) {
592 $this->record_event(
593 'provider_extension_deactivated',
594 array(
595 'provider_id' => $provider['id'],
596 'suggestion_id' => $provider['_suggestion_id'],
597 'provider_extension_slug' => $provider['plugin']['slug'],
598 )
599 );
600
601 // If the extension was also uninstalled, we can track that as well.
602 if ( ! empty( $provider['plugin']['status'] ) && PaymentsProviders::EXTENSION_NOT_INSTALLED === $provider['plugin']['status'] ) {
603 $this->record_event(
604 'provider_extension_uninstalled',
605 array(
606 'provider_id' => $provider['id'],
607 'suggestion_id' => $provider['_suggestion_id'],
608 'provider_extension_slug' => $provider['plugin']['slug'],
609 )
610 );
611 }
612 } elseif ( ! $old_snapshot['extension_active'] && $new_snapshot['extension_active'] ) {
613 $this->record_event(
614 'provider_extension_activated',
615 array(
616 'provider_id' => $provider['id'],
617 'suggestion_id' => $provider['_suggestion_id'],
618 'provider_extension_slug' => $provider['plugin']['slug'],
619 )
620 );
621 }
622
623 // Track account_connected change.
624 if ( $old_snapshot['account_connected'] && ! $new_snapshot['account_connected'] ) {
625 $this->record_event(
626 'provider_account_disconnected',
627 array(
628 'provider_id' => $provider['id'],
629 'suggestion_id' => $provider['_suggestion_id'],
630 'provider_extension_slug' => $provider['plugin']['slug'],
631 'provider_account_test_mode' => $old_snapshot['account_test_mode'] ? 'yes' : 'no',
632 )
633 );
634 } elseif ( ! $old_snapshot['account_connected'] && $new_snapshot['account_connected'] ) {
635 $this->record_event(
636 'provider_account_connected',
637 array(
638 'provider_id' => $provider['id'],
639 'suggestion_id' => $provider['_suggestion_id'],
640 'provider_extension_slug' => $provider['plugin']['slug'],
641 'provider_account_test_mode' => $new_snapshot['account_test_mode'] ? 'yes' : 'no',
642 )
643 );
644 }
645
646 // Track needs_setup change.
647 if ( $old_snapshot['needs_setup'] && ! $new_snapshot['needs_setup'] ) {
648 $this->record_event(
649 'provider_setup_completed',
650 array(
651 'provider_id' => $provider['id'],
652 'suggestion_id' => $provider['_suggestion_id'],
653 'provider_extension_slug' => $provider['plugin']['slug'],
654 )
655 );
656 } elseif ( ! $old_snapshot['needs_setup'] && $new_snapshot['needs_setup'] ) {
657 $this->record_event(
658 'provider_setup_required',
659 array(
660 'provider_id' => $provider['id'],
661 'suggestion_id' => $provider['_suggestion_id'],
662 'provider_extension_slug' => $provider['plugin']['slug'],
663 )
664 );
665 }
666
667 // Track payments test_mode change, but only if an account is connected.
668 if ( $new_snapshot['account_connected'] ) {
669 if ( $old_snapshot['test_mode'] && ! $new_snapshot['test_mode'] ) {
670 $this->record_event(
671 'provider_live_payments_enabled',
672 array(
673 'provider_id' => $provider['id'],
674 'suggestion_id' => $provider['_suggestion_id'],
675 'provider_extension_slug' => $provider['plugin']['slug'],
676 )
677 );
678 } elseif ( ! $old_snapshot['test_mode'] && $new_snapshot['test_mode'] ) {
679 $this->record_event(
680 'provider_test_payments_enabled',
681 array(
682 'provider_id' => $provider['id'],
683 'suggestion_id' => $provider['_suggestion_id'],
684 'provider_extension_slug' => $provider['plugin']['slug'],
685 )
686 );
687 }
688 }
689
690 // Track account_test_mode change, but only if the account is connected.
691 if ( $new_snapshot['account_connected'] ) {
692 if ( $old_snapshot['account_test_mode'] && ! $new_snapshot['account_test_mode'] ) {
693 $this->record_event(
694 'provider_account_live_mode_enabled',
695 array(
696 'provider_id' => $provider['id'],
697 'suggestion_id' => $provider['_suggestion_id'],
698 'provider_extension_slug' => $provider['plugin']['slug'],
699 )
700 );
701 } elseif ( ! $old_snapshot['account_test_mode'] && $new_snapshot['account_test_mode'] ) {
702 $this->record_event(
703 'provider_account_test_mode_enabled',
704 array(
705 'provider_id' => $provider['id'],
706 'suggestion_id' => $provider['_suggestion_id'],
707 'provider_extension_slug' => $provider['plugin']['slug'],
708 )
709 );
710 }
711 }
712 }
713 }
714