Exceptions
1 year ago
PaymentsProviders
3 months ago
SettingsUIPages
1 month ago
LegacySettingsPageAdapter.php
1 month 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
1 month ago
SettingsUIRequestContext.php
2 weeks ago
SettingsUISchema.php
2 weeks ago
Utils.php
2 months ago
PaymentsProviders.php
1622 lines
| 1 | <?php |
| 2 | declare( strict_types=1 ); |
| 3 | |
| 4 | namespace Automattic\WooCommerce\Internal\Admin\Settings; |
| 5 | |
| 6 | use Automattic\WooCommerce\Admin\PluginsHelper; |
| 7 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Affirm; |
| 8 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\AfterpayClearpay; |
| 9 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Airwallex; |
| 10 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\AmazonPay; |
| 11 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Antom; |
| 12 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Eway; |
| 13 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\GoCardless; |
| 14 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\HelioPay; |
| 15 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Klarna; |
| 16 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\KlarnaCheckout; |
| 17 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\MercadoPago; |
| 18 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Mollie; |
| 19 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Monei; |
| 20 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\NexiCheckout; |
| 21 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Payfast; |
| 22 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\PaymentGateway; |
| 23 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Paymob; |
| 24 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Payoneer; |
| 25 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\PayPal; |
| 26 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Paystack; |
| 27 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Paytrail; |
| 28 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\PayUIndia; |
| 29 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Razorpay; |
| 30 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Stripe; |
| 31 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Tilopay; |
| 32 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Visa; |
| 33 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\Vivacom; |
| 34 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WCCore; |
| 35 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments; |
| 36 | use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsService; |
| 37 | use Automattic\WooCommerce\Internal\Admin\Suggestions\PaymentsExtensionSuggestions as ExtensionSuggestions; |
| 38 | use Automattic\WooCommerce\Proxies\LegacyProxy; |
| 39 | use Exception; |
| 40 | use WC_Payment_Gateway; |
| 41 | use WC_Gateway_BACS; |
| 42 | use WC_Gateway_Cheque; |
| 43 | use WC_Gateway_COD; |
| 44 | use WC_Gateway_Paypal; |
| 45 | |
| 46 | defined( 'ABSPATH' ) || exit; |
| 47 | |
| 48 | /** |
| 49 | * Payments Providers class. |
| 50 | * |
| 51 | * @internal |
| 52 | */ |
| 53 | class PaymentsProviders { |
| 54 | |
| 55 | public const TYPE_GATEWAY = 'gateway'; |
| 56 | public const TYPE_OFFLINE_PM = 'offline_pm'; |
| 57 | public const TYPE_OFFLINE_PMS_GROUP = 'offline_pms_group'; |
| 58 | public const TYPE_SUGGESTION = 'suggestion'; |
| 59 | |
| 60 | public const OFFLINE_METHODS = array( WC_Gateway_BACS::ID, WC_Gateway_Cheque::ID, WC_Gateway_COD::ID ); |
| 61 | |
| 62 | public const EXTENSION_NOT_INSTALLED = 'not_installed'; |
| 63 | public const EXTENSION_INSTALLED = 'installed'; |
| 64 | public const EXTENSION_ACTIVE = 'active'; |
| 65 | |
| 66 | // For providers that are delivered through a plugin available on the WordPress.org repository. |
| 67 | public const EXTENSION_TYPE_WPORG = 'wporg'; |
| 68 | // For providers that are delivered through a must-use plugin. |
| 69 | public const EXTENSION_TYPE_MU_PLUGIN = 'mu_plugin'; |
| 70 | // For providers that are delivered through a theme. |
| 71 | public const EXTENSION_TYPE_THEME = 'theme'; |
| 72 | // For providers that are delivered through an unknown mechanism. |
| 73 | public const EXTENSION_TYPE_UNKNOWN = 'unknown'; |
| 74 | |
| 75 | public const PROVIDERS_ORDER_OPTION = 'woocommerce_gateway_order'; |
| 76 | public const SUGGESTION_ORDERING_PREFIX = '_wc_pes_'; |
| 77 | public const OFFLINE_METHODS_ORDERING_GROUP = '_wc_offline_payment_methods_group'; |
| 78 | |
| 79 | public const CATEGORY_EXPRESS_CHECKOUT = 'express_checkout'; |
| 80 | public const CATEGORY_BNPL = 'bnpl'; |
| 81 | public const CATEGORY_CRYPTO = 'crypto'; |
| 82 | public const CATEGORY_PSP = 'psp'; |
| 83 | |
| 84 | /* |
| 85 | * The provider link types. |
| 86 | * |
| 87 | * These are hints for the UI to determine if and how to display the link. |
| 88 | */ |
| 89 | public const LINK_TYPE_SUPPORT = 'support'; |
| 90 | public const LINK_TYPE_DOCS = 'documentation'; |
| 91 | public const LINK_TYPE_ABOUT = 'about'; |
| 92 | public const LINK_TYPE_TERMS = 'terms'; |
| 93 | public const LINK_TYPE_PRICING = 'pricing'; |
| 94 | |
| 95 | /** |
| 96 | * The map of gateway IDs to their respective provider classes. |
| 97 | * |
| 98 | * @var \class-string[] |
| 99 | */ |
| 100 | private array $payment_gateways_providers_class_map = array( |
| 101 | WC_Gateway_BACS::ID => WCCore::class, |
| 102 | WC_Gateway_Cheque::ID => WCCore::class, |
| 103 | WC_Gateway_COD::ID => WCCore::class, |
| 104 | WC_Gateway_Paypal::ID => WCCore::class, |
| 105 | 'woocommerce_payments' => WooPayments::class, |
| 106 | 'ppcp-gateway' => PayPal::class, |
| 107 | 'stripe' => Stripe::class, |
| 108 | 'stripe_*' => Stripe::class, |
| 109 | 'mollie' => Mollie::class, |
| 110 | 'mollie_wc_gateway_*' => Mollie::class, // Target all the Mollie gateways. |
| 111 | 'amazon_payments_advanced*' => AmazonPay::class, |
| 112 | 'woo-mercado-pago-*' => MercadoPago::class, |
| 113 | 'affirm' => Affirm::class, |
| 114 | 'klarna_payments' => Klarna::class, |
| 115 | 'afterpay' => AfterpayClearpay::class, |
| 116 | 'clearpay' => AfterpayClearpay::class, |
| 117 | 'antom_*' => Antom::class, |
| 118 | 'razorpay' => Razorpay::class, |
| 119 | 'paystack' => Paystack::class, |
| 120 | 'paystack-*' => Paystack::class, |
| 121 | 'payfast' => Payfast::class, |
| 122 | 'payoneer-*' => Payoneer::class, |
| 123 | 'payubiz' => PayUIndia::class, |
| 124 | 'paymob' => Paymob::class, |
| 125 | 'paymob-*' => Paymob::class, |
| 126 | 'airwallex_*' => Airwallex::class, |
| 127 | 'vivawallet*' => Vivacom::class, |
| 128 | 'tilopay' => Tilopay::class, |
| 129 | 'helio' => HelioPay::class, |
| 130 | 'paytrail' => Paytrail::class, |
| 131 | 'monei' => Monei::class, |
| 132 | 'monei_*' => Monei::class, |
| 133 | 'gocardless' => GoCardless::class, |
| 134 | 'kco' => KlarnaCheckout::class, |
| 135 | 'visa_acceptance_solutions_*' => Visa::class, |
| 136 | 'eway' => Eway::class, |
| 137 | 'dibs_easy' => NexiCheckout::class, |
| 138 | ); |
| 139 | |
| 140 | /** |
| 141 | * The map of payment extension suggestion IDs to their respective provider classes. |
| 142 | * |
| 143 | * This is used to instantiate providers to provide details for the payment extension suggestions, pre-attachment. |
| 144 | * |
| 145 | * @var \class-string[] |
| 146 | */ |
| 147 | private array $payment_extension_suggestions_providers_class_map = array( |
| 148 | ExtensionSuggestions::WOOPAYMENTS => WooPayments::class, |
| 149 | ExtensionSuggestions::PAYPAL_FULL_STACK => PayPal::class, |
| 150 | ExtensionSuggestions::PAYPAL_WALLET => PayPal::class, |
| 151 | ExtensionSuggestions::STRIPE => Stripe::class, |
| 152 | ExtensionSuggestions::MOLLIE => Mollie::class, |
| 153 | ExtensionSuggestions::AMAZON_PAY => AmazonPay::class, |
| 154 | ExtensionSuggestions::MERCADO_PAGO => MercadoPago::class, |
| 155 | ExtensionSuggestions::AFFIRM => Affirm::class, |
| 156 | ExtensionSuggestions::KLARNA => Klarna::class, |
| 157 | ExtensionSuggestions::AFTERPAY => AfterpayClearpay::class, |
| 158 | ExtensionSuggestions::CLEARPAY => AfterpayClearpay::class, |
| 159 | ExtensionSuggestions::ANTOM => Antom::class, |
| 160 | ExtensionSuggestions::RAZORPAY => Razorpay::class, |
| 161 | ExtensionSuggestions::PAYSTACK => Paystack::class, |
| 162 | ExtensionSuggestions::PAYFAST => Payfast::class, |
| 163 | ExtensionSuggestions::PAYONEER => Payoneer::class, |
| 164 | ExtensionSuggestions::PAYU_INDIA => PayUIndia::class, |
| 165 | ExtensionSuggestions::PAYMOB => Paymob::class, |
| 166 | ExtensionSuggestions::AIRWALLEX => Airwallex::class, |
| 167 | ExtensionSuggestions::VIVA_WALLET => Vivacom::class, |
| 168 | ExtensionSuggestions::TILOPAY => Tilopay::class, |
| 169 | ExtensionSuggestions::HELIOPAY => HelioPay::class, |
| 170 | ExtensionSuggestions::PAYTRAIL => Paytrail::class, |
| 171 | ExtensionSuggestions::MONEI => Monei::class, |
| 172 | ExtensionSuggestions::GOCARDLESS => GoCardless::class, |
| 173 | ExtensionSuggestions::KLARNA_CHECKOUT => KlarnaCheckout::class, |
| 174 | ExtensionSuggestions::VISA => Visa::class, |
| 175 | ExtensionSuggestions::EWAY => Eway::class, |
| 176 | ExtensionSuggestions::NEXI_CHECKOUT => NexiCheckout::class, |
| 177 | ); |
| 178 | |
| 179 | /** |
| 180 | * The instances of the payment providers. |
| 181 | * |
| 182 | * @var PaymentGateway[] |
| 183 | */ |
| 184 | private array $instances = array(); |
| 185 | |
| 186 | /** |
| 187 | * The memoized payment gateways to avoid computing the list multiple times during a request. |
| 188 | * |
| 189 | * @var array |
| 190 | */ |
| 191 | private array $payment_gateways_memo = array(); |
| 192 | |
| 193 | /** |
| 194 | * The memoized payment gateways for display to avoid computing the list multiple times during a request. |
| 195 | * |
| 196 | * This is especially important since it avoids triggering the legacy action multiple times during a request. |
| 197 | * |
| 198 | * @var array |
| 199 | */ |
| 200 | private array $payment_gateways_for_display_memo = array(); |
| 201 | |
| 202 | /** |
| 203 | * The payment extension suggestions service. |
| 204 | * |
| 205 | * @var ExtensionSuggestions |
| 206 | */ |
| 207 | private ExtensionSuggestions $extension_suggestions; |
| 208 | |
| 209 | /** |
| 210 | * The LegacyProxy instance. |
| 211 | * |
| 212 | * @var LegacyProxy |
| 213 | */ |
| 214 | private LegacyProxy $proxy; |
| 215 | |
| 216 | /** |
| 217 | * Initialize the class instance. |
| 218 | * |
| 219 | * @param ExtensionSuggestions $payment_extension_suggestions The payment extension suggestions service. |
| 220 | * @param LegacyProxy $proxy The LegacyProxy instance. |
| 221 | * |
| 222 | * @internal |
| 223 | */ |
| 224 | final public function init( ExtensionSuggestions $payment_extension_suggestions, LegacyProxy $proxy ): void { |
| 225 | $this->extension_suggestions = $payment_extension_suggestions; |
| 226 | $this->proxy = $proxy; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Get the payment gateways for the settings page. |
| 231 | * |
| 232 | * We apply the same actions and logic that the non-React Payments settings page uses to get the gateways. |
| 233 | * This way we maintain backwards compatibility. |
| 234 | * |
| 235 | * @param bool $for_display Whether the payment gateway list is intended for display purposes. |
| 236 | * This triggers the legacy `woocommerce_admin_field_payment_gateways` action and |
| 237 | * the exclusion of "shell" gateways. |
| 238 | * Default is true. |
| 239 | * @param string $country_code Optional. The country code for which the payment gateways are being generated. |
| 240 | * This should be an ISO 3166-1 alpha-2 country code. |
| 241 | * |
| 242 | * @return array The payment gateway objects list. |
| 243 | */ |
| 244 | public function get_payment_gateways( bool $for_display = true, string $country_code = '' ): array { |
| 245 | // Normalize the country code to uppercase. |
| 246 | $country_code = strtoupper( $country_code ); |
| 247 | |
| 248 | // If we are asked for a display gateways list, we need to fire legacy actions and filter out "shells". |
| 249 | if ( $for_display ) { |
| 250 | if ( isset( $this->payment_gateways_for_display_memo[ $country_code ] ) ) { |
| 251 | return $this->payment_gateways_for_display_memo[ $country_code ]; |
| 252 | } |
| 253 | |
| 254 | // We don't want to output anything from the action. So we buffer it and discard it. |
| 255 | // We just want to give the payment extensions a chance to adjust the payment gateways list for the settings page. |
| 256 | // This is primarily for backwards compatibility. |
| 257 | ob_start(); |
| 258 | /** |
| 259 | * Fires before the payment gateways settings fields are rendered. |
| 260 | * |
| 261 | * @since 1.5.7 |
| 262 | */ |
| 263 | do_action( 'woocommerce_admin_field_payment_gateways' ); |
| 264 | ob_end_clean(); |
| 265 | |
| 266 | // Get all payment gateways, ordered by the user. |
| 267 | $payment_gateways = WC()->payment_gateways()->payment_gateways; |
| 268 | |
| 269 | // Handle edge-cases for certain providers. |
| 270 | $payment_gateways = $this->handle_non_standard_registration_for_payment_gateways( $payment_gateways ); |
| 271 | |
| 272 | // Remove "shell" gateways from the list. |
| 273 | $payment_gateways = $this->remove_shell_payment_gateways( $payment_gateways, $country_code ); |
| 274 | |
| 275 | // Store the entire payment gateways list for display for later use. |
| 276 | $this->payment_gateways_for_display_memo[ $country_code ] = $payment_gateways; |
| 277 | |
| 278 | return $payment_gateways; |
| 279 | } |
| 280 | |
| 281 | // We were asked for the raw payment gateways list. |
| 282 | if ( isset( $this->payment_gateways_memo[ $country_code ] ) ) { |
| 283 | return $this->payment_gateways_memo[ $country_code ]; |
| 284 | } |
| 285 | |
| 286 | // Get all payment gateways, ordered by the user. |
| 287 | $payment_gateways = WC()->payment_gateways()->payment_gateways; |
| 288 | |
| 289 | // Handle edge-cases for certain providers. |
| 290 | $payment_gateways = $this->handle_non_standard_registration_for_payment_gateways( $payment_gateways ); |
| 291 | |
| 292 | // Store the entire payment gateways list for later use. |
| 293 | $this->payment_gateways_memo[ $country_code ] = $payment_gateways; |
| 294 | |
| 295 | return $payment_gateways; |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * Remove "shell" gateways from the provided payment gateways list. |
| 300 | * |
| 301 | * We consider a gateway to be a "shell" if it has no WC admin title or description. |
| 302 | * The removal is done in a way that ensures we do not remove all gateways from an extension, |
| 303 | * thus preventing user access to the settings page(s) for that extension. |
| 304 | * |
| 305 | * @param array $payment_gateways The payment gateways list to process. |
| 306 | * @param string $country_code Optional. The country code for which the payment gateways are being generated. |
| 307 | * This should be an ISO 3166-1 alpha-2 country code. |
| 308 | * |
| 309 | * @return array The processed payment gateways list. |
| 310 | */ |
| 311 | public function remove_shell_payment_gateways( array $payment_gateways, string $country_code = '' ): array { |
| 312 | // Normalize the country code to uppercase. |
| 313 | $country_code = strtoupper( $country_code ); |
| 314 | |
| 315 | $grouped_payment_gateways = $this->group_gateways_by_extension( $payment_gateways, $country_code ); |
| 316 | return array_filter( |
| 317 | $payment_gateways, |
| 318 | function ( $gateway ) use ( $grouped_payment_gateways, $country_code ) { |
| 319 | // If the gateway is a shell, we only remove it if there are other, non-shell gateways from that extension. |
| 320 | // This is to avoid removing all the gateways registered by an extension and |
| 321 | // preventing user access to the settings page(s) for that extension. |
| 322 | if ( $this->is_shell_payment_gateway( $gateway ) ) { |
| 323 | $gateway_details = $this->get_payment_gateway_details( $gateway, 0, $country_code ); |
| 324 | // In case we don't have the needed extension details, |
| 325 | // we allow the gateway to be displayed (aka better safe than sorry). |
| 326 | if ( empty( $gateway_details ) || ! isset( $gateway_details['plugin'] ) || empty( $gateway_details['plugin']['file'] ) ) { |
| 327 | return true; |
| 328 | } |
| 329 | |
| 330 | if ( empty( $grouped_payment_gateways[ $gateway_details['plugin']['file'] ] ) || |
| 331 | count( $grouped_payment_gateways[ $gateway_details['plugin']['file'] ] ) <= 1 ) { |
| 332 | // If there are no other gateways from the same extension, we let the shell gateway be displayed. |
| 333 | return true; |
| 334 | } |
| 335 | |
| 336 | // Check if there are any other gateways from the same extension that are NOT shells. |
| 337 | foreach ( $grouped_payment_gateways[ $gateway_details['plugin']['file'] ] as $extension_gateway ) { |
| 338 | if ( ! $this->is_shell_payment_gateway( $extension_gateway ) ) { |
| 339 | // If we found a gateway from the same extension that is not a shell, |
| 340 | // we hide all shells from that extension. |
| 341 | return false; |
| 342 | } |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | // By this point, we know that the gateway is not a shell or that it is a shell |
| 347 | // but there are no non-shell gateways from the same extension. Include it. |
| 348 | return true; |
| 349 | } |
| 350 | ); |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * Get the payment gateway provider instance. |
| 355 | * |
| 356 | * @param string $gateway_id The gateway ID. |
| 357 | * |
| 358 | * @return PaymentGateway The payment gateway provider instance. |
| 359 | * Will return the general provider of no specific provider is found. |
| 360 | */ |
| 361 | public function get_payment_gateway_provider_instance( string $gateway_id ): PaymentGateway { |
| 362 | if ( isset( $this->instances[ $gateway_id ] ) ) { |
| 363 | return $this->instances[ $gateway_id ]; |
| 364 | } |
| 365 | |
| 366 | /** |
| 367 | * The provider class for the gateway. |
| 368 | * |
| 369 | * @var class-string<PaymentGateway>|null $provider_class |
| 370 | */ |
| 371 | $provider_class = null; |
| 372 | if ( isset( $this->payment_gateways_providers_class_map[ $gateway_id ] ) ) { |
| 373 | $provider_class = $this->payment_gateways_providers_class_map[ $gateway_id ]; |
| 374 | } else { |
| 375 | // Check for wildcard mappings. |
| 376 | foreach ( $this->payment_gateways_providers_class_map as $gateway_id_pattern => $mapped_class ) { |
| 377 | // Try to see if we have a wildcard mapping and if the gateway ID matches it. |
| 378 | // Use the first found match. |
| 379 | if ( false !== strpos( $gateway_id_pattern, '*' ) ) { |
| 380 | $gateway_id_pattern = str_replace( '*', '.*', $gateway_id_pattern ); |
| 381 | if ( preg_match( '/^' . $gateway_id_pattern . '$/', $gateway_id ) ) { |
| 382 | $provider_class = $mapped_class; |
| 383 | break; |
| 384 | } |
| 385 | } |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | // Check that the provider class extends the PaymentGateway class. |
| 390 | if ( ! is_null( $provider_class ) && ! is_subclass_of( $provider_class, PaymentGateway::class ) ) { |
| 391 | wc_doing_it_wrong( |
| 392 | __METHOD__, |
| 393 | sprintf( |
| 394 | /* translators: %s: Gateway ID. */ |
| 395 | esc_html__( 'The provider class for gateway ID "%s" must extend the PaymentGateway class.', 'woocommerce' ), |
| 396 | $gateway_id |
| 397 | ), |
| 398 | '10.4.0' |
| 399 | ); |
| 400 | // Return the generic provider as a fallback. |
| 401 | $provider_class = null; |
| 402 | } |
| 403 | |
| 404 | // If the gateway ID is not mapped to a provider class, return the generic provider. |
| 405 | if ( is_null( $provider_class ) ) { |
| 406 | if ( ! isset( $this->instances['generic'] ) ) { |
| 407 | $this->instances['generic'] = new PaymentGateway( $this->proxy ); |
| 408 | } |
| 409 | |
| 410 | return $this->instances['generic']; |
| 411 | } |
| 412 | |
| 413 | $this->instances[ $gateway_id ] = new $provider_class( $this->proxy ); |
| 414 | |
| 415 | return $this->instances[ $gateway_id ]; |
| 416 | } |
| 417 | |
| 418 | /** |
| 419 | * Get the payment extension suggestion (PES) provider instance. |
| 420 | * |
| 421 | * @param string $pes_id The payment extension suggestion ID. |
| 422 | * |
| 423 | * @return PaymentGateway The payment extension suggestion provider instance. |
| 424 | * Will return the general provider of no specific provider is found. |
| 425 | */ |
| 426 | public function get_payment_extension_suggestion_provider_instance( string $pes_id ): PaymentGateway { |
| 427 | if ( isset( $this->instances[ $pes_id ] ) ) { |
| 428 | return $this->instances[ $pes_id ]; |
| 429 | } |
| 430 | |
| 431 | /** |
| 432 | * The provider class for the payment extension suggestion (PES). |
| 433 | * |
| 434 | * @var class-string<PaymentGateway>|null $provider_class |
| 435 | */ |
| 436 | $provider_class = null; |
| 437 | if ( isset( $this->payment_extension_suggestions_providers_class_map[ $pes_id ] ) ) { |
| 438 | if ( ! is_subclass_of( $this->payment_extension_suggestions_providers_class_map[ $pes_id ], PaymentGateway::class ) ) { |
| 439 | wc_doing_it_wrong( |
| 440 | __METHOD__, |
| 441 | sprintf( |
| 442 | /* translators: %s: Payment extension suggestion ID. */ |
| 443 | esc_html__( 'The provider class for payment extension suggestion ID "%s" must extend the PaymentGateway class.', 'woocommerce' ), |
| 444 | $pes_id |
| 445 | ), |
| 446 | '10.4.0' |
| 447 | ); |
| 448 | // Return the generic provider as a fallback. |
| 449 | } else { |
| 450 | $provider_class = $this->payment_extension_suggestions_providers_class_map[ $pes_id ]; |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | // If the gateway ID is not mapped to a provider class, return the generic provider. |
| 455 | if ( is_null( $provider_class ) ) { |
| 456 | if ( ! isset( $this->instances['generic'] ) ) { |
| 457 | $this->instances['generic'] = new PaymentGateway( $this->proxy ); |
| 458 | } |
| 459 | |
| 460 | return $this->instances['generic']; |
| 461 | } |
| 462 | |
| 463 | $this->instances[ $pes_id ] = new $provider_class( $this->proxy ); |
| 464 | |
| 465 | return $this->instances[ $pes_id ]; |
| 466 | } |
| 467 | |
| 468 | /** |
| 469 | * Get the payment gateways details. |
| 470 | * |
| 471 | * @param WC_Payment_Gateway $payment_gateway The payment gateway object. |
| 472 | * @param int $payment_gateway_order The order of the payment gateway. |
| 473 | * @param string $country_code Optional. The country code for which the details are being gathered. |
| 474 | * This should be an ISO 3166-1 alpha-2 country code. |
| 475 | * |
| 476 | * @return array The payment gateway details. |
| 477 | */ |
| 478 | public function get_payment_gateway_details( WC_Payment_Gateway $payment_gateway, int $payment_gateway_order, string $country_code = '' ): array { |
| 479 | // Normalize the country code to uppercase. |
| 480 | $country_code = strtoupper( $country_code ); |
| 481 | |
| 482 | return $this->enhance_payment_gateway_details( |
| 483 | $this->get_payment_gateway_base_details( $payment_gateway, $payment_gateway_order, $country_code ), |
| 484 | $payment_gateway, |
| 485 | $country_code |
| 486 | ); |
| 487 | } |
| 488 | |
| 489 | /** |
| 490 | * Get the payment gateways details from the object. |
| 491 | * |
| 492 | * @param WC_Payment_Gateway $payment_gateway The payment gateway object. |
| 493 | * @param int $payment_gateway_order The order of the payment gateway. |
| 494 | * @param string $country_code Optional. The country code for which the details are being gathered. |
| 495 | * This should be an ISO 3166-1 alpha-2 country code. |
| 496 | * |
| 497 | * @return array The payment gateway base details. |
| 498 | */ |
| 499 | public function get_payment_gateway_base_details( WC_Payment_Gateway $payment_gateway, int $payment_gateway_order, string $country_code = '' ): array { |
| 500 | // Normalize the country code to uppercase. |
| 501 | $country_code = strtoupper( $country_code ); |
| 502 | |
| 503 | $provider = $this->get_payment_gateway_provider_instance( $payment_gateway->id ); |
| 504 | |
| 505 | return $provider->get_details( $payment_gateway, $payment_gateway_order, $country_code ); |
| 506 | } |
| 507 | |
| 508 | /** |
| 509 | * Get the source plugin slug of a payment gateway instance. |
| 510 | * |
| 511 | * @param WC_Payment_Gateway $payment_gateway The payment gateway object. |
| 512 | * |
| 513 | * @return string The plugin slug of the payment gateway. |
| 514 | * Empty string if a plugin slug could not be determined. |
| 515 | */ |
| 516 | public function get_payment_gateway_plugin_slug( WC_Payment_Gateway $payment_gateway ): string { |
| 517 | $provider = $this->get_payment_gateway_provider_instance( $payment_gateway->id ); |
| 518 | |
| 519 | return $provider->get_plugin_slug( $payment_gateway ); |
| 520 | } |
| 521 | |
| 522 | /** |
| 523 | * Get the plugin file of payment gateway, without the .php extension. |
| 524 | * |
| 525 | * This is useful for the WP API, which expects the plugin file without the .php extension. |
| 526 | * |
| 527 | * @param WC_Payment_Gateway $payment_gateway The payment gateway object. |
| 528 | * @param string $plugin_slug Optional. The payment gateway plugin slug to use directly. |
| 529 | * |
| 530 | * @return string The plugin file corresponding to the payment gateway plugin. Does not include the .php extension. |
| 531 | */ |
| 532 | public function get_payment_gateway_plugin_file( WC_Payment_Gateway $payment_gateway, string $plugin_slug = '' ): string { |
| 533 | $provider = $this->get_payment_gateway_provider_instance( $payment_gateway->id ); |
| 534 | |
| 535 | return $provider->get_plugin_file( $payment_gateway, $plugin_slug ); |
| 536 | } |
| 537 | |
| 538 | /** |
| 539 | * Get the offline payment methods gateways. |
| 540 | * |
| 541 | * @return array The registered offline payment methods gateways keyed by their global gateways list order/index. |
| 542 | */ |
| 543 | public function get_offline_payment_methods_gateways(): array { |
| 544 | return array_filter( |
| 545 | $this->get_payment_gateways( false ), // We request the raw gateways list to get the global order/index. |
| 546 | function ( $gateway ) { |
| 547 | return $this->is_offline_payment_method( $gateway->id ); |
| 548 | } |
| 549 | ); |
| 550 | } |
| 551 | |
| 552 | /** |
| 553 | * Check if a payment gateway is an offline payment method. |
| 554 | * |
| 555 | * @param string $id The ID of the payment gateway. |
| 556 | * |
| 557 | * @return bool True if the payment gateway is an offline payment method, false otherwise. |
| 558 | */ |
| 559 | public function is_offline_payment_method( string $id ): bool { |
| 560 | return in_array( $id, self::OFFLINE_METHODS, true ); |
| 561 | } |
| 562 | |
| 563 | /** |
| 564 | * Check if the offline payment methods group is the last non-offline entry in an order map. |
| 565 | * |
| 566 | * This is used to detect whether the merchant has customized the provider ordering. |
| 567 | * If the offline group is still at the bottom (its default position), new gateways |
| 568 | * should be inserted above it. If the merchant has moved it, we respect their layout |
| 569 | * and append new gateways at the end. |
| 570 | * |
| 571 | * @param array $order_map The payment providers order map. |
| 572 | * |
| 573 | * @return bool True if the offline group is the last non-offline entry, false otherwise. |
| 574 | */ |
| 575 | public function is_offline_group_last( array $order_map ): bool { |
| 576 | if ( ! isset( $order_map[ self::OFFLINE_METHODS_ORDERING_GROUP ] ) ) { |
| 577 | return false; |
| 578 | } |
| 579 | |
| 580 | $offline_group_order = $order_map[ self::OFFLINE_METHODS_ORDERING_GROUP ]; |
| 581 | |
| 582 | // Check if any non-offline, non-suggestion entry has an order higher than the offline group. |
| 583 | foreach ( $order_map as $id => $order ) { |
| 584 | if ( self::OFFLINE_METHODS_ORDERING_GROUP === $id ) { |
| 585 | continue; |
| 586 | } |
| 587 | if ( $this->is_offline_payment_method( $id ) ) { |
| 588 | continue; |
| 589 | } |
| 590 | if ( $this->is_suggestion_order_map_id( $id ) ) { |
| 591 | continue; |
| 592 | } |
| 593 | if ( $order > $offline_group_order ) { |
| 594 | return false; |
| 595 | } |
| 596 | } |
| 597 | |
| 598 | return true; |
| 599 | } |
| 600 | |
| 601 | /** |
| 602 | * Add a new gateway to an order map with offline-awareness. |
| 603 | * |
| 604 | * If the offline payment methods group is the last non-offline, non-suggestion entry, |
| 605 | * the gateway is placed above it. Otherwise, it is appended at the end. |
| 606 | * |
| 607 | * This is the single source of truth for new gateway placement logic, |
| 608 | * used by both the display path (Payments) and the persistence path (enhance_order_map). |
| 609 | * |
| 610 | * @param array $order_map The payment providers order map. |
| 611 | * @param string $id The gateway ID to add. |
| 612 | * |
| 613 | * @return array The updated order map. |
| 614 | */ |
| 615 | public function order_map_add_gateway( array $order_map, string $id ): array { |
| 616 | if ( $this->is_offline_group_last( $order_map ) ) { |
| 617 | return Utils::order_map_add_at_order( |
| 618 | $order_map, |
| 619 | $id, |
| 620 | $order_map[ self::OFFLINE_METHODS_ORDERING_GROUP ] |
| 621 | ); |
| 622 | } |
| 623 | |
| 624 | return Utils::order_map_add_at_order( $order_map, $id, empty( $order_map ) ? 0 : max( $order_map ) + 1 ); |
| 625 | } |
| 626 | |
| 627 | /** |
| 628 | * Check if a payment gateway is a shell payment gateway. |
| 629 | * |
| 630 | * A shell payment gateway is generally one that has no method title or description. |
| 631 | * This is used to identify gateways that are not intended for display in the admin UI. |
| 632 | * |
| 633 | * @param WC_Payment_Gateway $gateway The payment gateway object. |
| 634 | * |
| 635 | * @return bool True if the payment gateway is a shell, false otherwise. |
| 636 | */ |
| 637 | public function is_shell_payment_gateway( WC_Payment_Gateway $gateway ): bool { |
| 638 | return ( empty( $gateway->get_method_title() ) && empty( $gateway->get_method_description() ) ) || |
| 639 | // Special case for WooPayments gateways that are not the main one: their method title is "WooPayments", |
| 640 | // but their ID is made up of the main gateway ID and a suffix for the payment method. |
| 641 | ( 'WooPayments' === $gateway->get_method_title() && str_starts_with( $gateway->id, WooPaymentsService::GATEWAY_ID . '_' ) ); |
| 642 | } |
| 643 | |
| 644 | /** |
| 645 | * Get the payment extension suggestions for the given location. |
| 646 | * |
| 647 | * @param string $location The location for which the suggestions are being fetched. |
| 648 | * @param string $context Optional. The context ID of where these extensions are being used. |
| 649 | * |
| 650 | * @return array[] The payment extension suggestions for the given location, split into preferred and other. |
| 651 | * @throws Exception If there are malformed or invalid suggestions. |
| 652 | */ |
| 653 | public function get_extension_suggestions( string $location, string $context = '' ): array { |
| 654 | // Normalize the location to uppercase. |
| 655 | $location = strtoupper( $location ); |
| 656 | |
| 657 | $preferred_psp = null; |
| 658 | $preferred_apm = null; |
| 659 | $preferred_offline_psp = null; |
| 660 | $other = array(); |
| 661 | |
| 662 | $extensions = $this->extension_suggestions->get_country_extensions( $location, $context ); |
| 663 | // Sort them by _priority. |
| 664 | usort( |
| 665 | $extensions, |
| 666 | function ( $a, $b ) { |
| 667 | return $a['_priority'] <=> $b['_priority']; |
| 668 | } |
| 669 | ); |
| 670 | |
| 671 | $has_enabled_ecommerce_gateways = $this->has_enabled_ecommerce_gateways(); |
| 672 | |
| 673 | // Keep track of the active extensions. |
| 674 | $active_extensions = array(); |
| 675 | |
| 676 | foreach ( $extensions as $extension ) { |
| 677 | $extension = $this->enhance_extension_suggestion( $extension ); |
| 678 | |
| 679 | if ( self::EXTENSION_ACTIVE === $extension['plugin']['status'] ) { |
| 680 | // If the suggested extension is active, we no longer suggest it. |
| 681 | // But remember it for later. |
| 682 | $active_extensions[] = $extension['id']; |
| 683 | continue; |
| 684 | } |
| 685 | |
| 686 | // Determine if the suggestion is preferred or not by looking at its tags. |
| 687 | $is_preferred = in_array( ExtensionSuggestions::TAG_PREFERRED, $extension['tags'], true ); |
| 688 | |
| 689 | // Determine if the suggestion is hidden (from the preferred locations). |
| 690 | $is_hidden = $this->is_payment_extension_suggestion_hidden( $extension ); |
| 691 | |
| 692 | if ( ! $is_hidden && $is_preferred ) { |
| 693 | // If we don't have a preferred offline payments PSP and the suggestion is an offline payments preferred PSP, |
| 694 | // add it to the preferred list. |
| 695 | // Check this first so we don't inadvertently "fill" the preferred PSP slot. |
| 696 | if ( empty( $preferred_offline_psp ) && |
| 697 | ExtensionSuggestions::TYPE_PSP === $extension['_type'] && |
| 698 | in_array( ExtensionSuggestions::TAG_PREFERRED_OFFLINE, $extension['tags'], true ) ) { |
| 699 | |
| 700 | $preferred_offline_psp = $extension; |
| 701 | continue; |
| 702 | } |
| 703 | |
| 704 | // If we don't have a preferred PSP and the suggestion is a preferred PSP, add it to the preferred list. |
| 705 | if ( empty( $preferred_psp ) && ExtensionSuggestions::TYPE_PSP === $extension['_type'] ) { |
| 706 | $preferred_psp = $extension; |
| 707 | continue; |
| 708 | } |
| 709 | |
| 710 | // If we don't have a preferred APM and the suggestion is a preferred APM, add it to the preferred list. |
| 711 | // In the preferred APM slot we might surface APMs but also Express Checkouts (PayPal Wallet). |
| 712 | if ( empty( $preferred_apm ) && |
| 713 | in_array( $extension['_type'], array( ExtensionSuggestions::TYPE_APM, ExtensionSuggestions::TYPE_EXPRESS_CHECKOUT ), true ) ) { |
| 714 | |
| 715 | $preferred_apm = $extension; |
| 716 | continue; |
| 717 | } |
| 718 | } |
| 719 | |
| 720 | if ( $is_hidden && |
| 721 | ExtensionSuggestions::TYPE_APM === $extension['_type'] && |
| 722 | ExtensionSuggestions::PAYPAL_FULL_STACK === $extension['id'] ) { |
| 723 | // If the PayPal Full Stack suggestion is hidden, we no longer suggest it, |
| 724 | // because we have the PayPal Express Checkout (Wallet) suggestion. |
| 725 | continue; |
| 726 | } |
| 727 | |
| 728 | // If there are no enabled ecommerce gateways (no PSP selected), |
| 729 | // we don't suggest express checkout, BNPL, or crypto extensions. |
| 730 | if ( ! $has_enabled_ecommerce_gateways && |
| 731 | in_array( $extension['_type'], array( ExtensionSuggestions::TYPE_EXPRESS_CHECKOUT, ExtensionSuggestions::TYPE_BNPL, ExtensionSuggestions::TYPE_CRYPTO ), true ) |
| 732 | ) { |
| 733 | continue; |
| 734 | } |
| 735 | |
| 736 | // If WooPayments or Stripe is active, we don't suggest other BNPLs. |
| 737 | // Note: Affirm is available in the UK even with WooPayments or Stripe active |
| 738 | // because Stripe does not support it there, yet. |
| 739 | if ( ExtensionSuggestions::TYPE_BNPL === $extension['_type'] && |
| 740 | ( |
| 741 | in_array( ExtensionSuggestions::STRIPE, $active_extensions, true ) || |
| 742 | in_array( ExtensionSuggestions::WOOPAYMENTS, $active_extensions, true ) |
| 743 | ) && |
| 744 | ! ( |
| 745 | ExtensionSuggestions::AFFIRM === $extension['id'] && |
| 746 | 'GB' === $location |
| 747 | ) |
| 748 | ) { |
| 749 | continue; |
| 750 | } |
| 751 | |
| 752 | // If we made it to this point, the suggestion goes into the other list. |
| 753 | // But first, make sure there isn't already an extension added to the other list with the same plugin slug. |
| 754 | // This can happen if the same extension is suggested as both a PSP and an APM. |
| 755 | // The first entry that we encounter is the one that we keep. |
| 756 | $extension_slug = $extension['plugin']['slug']; |
| 757 | $extension_exists = array_filter( |
| 758 | $other, |
| 759 | function ( $suggestion ) use ( $extension_slug ) { |
| 760 | return $suggestion['plugin']['slug'] === $extension_slug; |
| 761 | } |
| 762 | ); |
| 763 | if ( ! empty( $extension_exists ) ) { |
| 764 | continue; |
| 765 | } |
| 766 | |
| 767 | $other[] = $extension; |
| 768 | } |
| 769 | |
| 770 | // Make sure that the preferred suggestions are not among the other list by removing any entries with their plugin slug. |
| 771 | $other = array_values( |
| 772 | array_filter( |
| 773 | $other, |
| 774 | function ( $suggestion ) use ( $preferred_psp, $preferred_apm ) { |
| 775 | return ( empty( $preferred_psp ) || $suggestion['plugin']['slug'] !== $preferred_psp['plugin']['slug'] ) && |
| 776 | ( empty( $preferred_apm ) || $suggestion['plugin']['slug'] !== $preferred_apm['plugin']['slug'] ); |
| 777 | } |
| 778 | ) |
| 779 | ); |
| 780 | |
| 781 | // The preferred PSP gets a recommended tag that instructs the UI to highlight it further. |
| 782 | if ( ! empty( $preferred_psp ) ) { |
| 783 | $preferred_psp['tags'][] = ExtensionSuggestions::TAG_RECOMMENDED; |
| 784 | } |
| 785 | |
| 786 | return array( |
| 787 | 'preferred' => array_values( |
| 788 | array_filter( |
| 789 | array( |
| 790 | // The PSP should naturally have a higher priority than the APM, with the preferred offline PSP last. |
| 791 | // No need to impose a specific order here. |
| 792 | $preferred_psp, |
| 793 | $preferred_apm, |
| 794 | $preferred_offline_psp, |
| 795 | ) |
| 796 | ) |
| 797 | ), |
| 798 | 'other' => $other, |
| 799 | ); |
| 800 | } |
| 801 | |
| 802 | /** |
| 803 | * Get a payment extension suggestion by ID. |
| 804 | * |
| 805 | * @param string $id The ID of the payment extension suggestion. |
| 806 | * |
| 807 | * @return ?array The payment extension suggestion details, or null if not found. |
| 808 | */ |
| 809 | public function get_extension_suggestion_by_id( string $id ): ?array { |
| 810 | $suggestion = $this->extension_suggestions->get_by_id( $id ); |
| 811 | if ( ! is_null( $suggestion ) ) { |
| 812 | // Enhance the suggestion details. |
| 813 | $suggestion = $this->enhance_extension_suggestion( $suggestion ); |
| 814 | } |
| 815 | |
| 816 | return $suggestion; |
| 817 | } |
| 818 | |
| 819 | /** |
| 820 | * Get a payment extension suggestion by plugin slug. |
| 821 | * |
| 822 | * @param string $slug The plugin slug of the payment extension suggestion. |
| 823 | * @param string $country_code Optional. The business location country code to get the suggestions for. |
| 824 | * |
| 825 | * @return ?array The payment extension suggestion details, or null if not found. |
| 826 | */ |
| 827 | public function get_extension_suggestion_by_plugin_slug( string $slug, string $country_code = '' ): ?array { |
| 828 | // Normalize the country code to uppercase. |
| 829 | $country_code = strtoupper( $country_code ); |
| 830 | |
| 831 | $suggestion = $this->extension_suggestions->get_by_plugin_slug( $slug, $country_code, Payments::SUGGESTIONS_CONTEXT ); |
| 832 | if ( ! is_null( $suggestion ) ) { |
| 833 | // Enhance the suggestion details. |
| 834 | $suggestion = $this->enhance_extension_suggestion( $suggestion ); |
| 835 | } |
| 836 | |
| 837 | return $suggestion; |
| 838 | } |
| 839 | |
| 840 | /** |
| 841 | * Attach a payment extension suggestion. |
| 842 | * |
| 843 | * Attachment is a broad concept that can mean different things depending on the suggestion. |
| 844 | * Currently, we use it to record the extension installation. This is why we expect to receive |
| 845 | * instructions to record attachment when the extension is installed. |
| 846 | * |
| 847 | * @param string $id The ID of the payment extension suggestion to attach. |
| 848 | * |
| 849 | * @return bool True if the suggestion was successfully marked as attached, false otherwise. |
| 850 | * @throws Exception If the suggestion ID is invalid. |
| 851 | */ |
| 852 | public function attach_extension_suggestion( string $id ): bool { |
| 853 | // We may receive a suggestion ID that is actually an order map ID used in the settings page providers list. |
| 854 | // Extract the suggestion ID from the order map ID. |
| 855 | if ( $this->is_suggestion_order_map_id( $id ) ) { |
| 856 | $id = $this->get_suggestion_id_from_order_map_id( $id ); |
| 857 | } |
| 858 | |
| 859 | $suggestion = $this->get_extension_suggestion_by_id( $id ); |
| 860 | if ( is_null( $suggestion ) ) { |
| 861 | throw new Exception( esc_html__( 'Invalid suggestion ID.', 'woocommerce' ) ); |
| 862 | } |
| 863 | |
| 864 | $payments_nox_profile = get_option( Payments::PAYMENTS_NOX_PROFILE_KEY, array() ); |
| 865 | if ( empty( $payments_nox_profile ) ) { |
| 866 | $payments_nox_profile = array(); |
| 867 | } else { |
| 868 | $payments_nox_profile = maybe_unserialize( $payments_nox_profile ); |
| 869 | } |
| 870 | |
| 871 | // Check if it is already marked as attached. |
| 872 | if ( ! empty( $payments_nox_profile['suggestions'][ $id ]['attached']['timestamp'] ) ) { |
| 873 | return true; |
| 874 | } |
| 875 | |
| 876 | // Mark the suggestion as attached. |
| 877 | if ( empty( $payments_nox_profile['suggestions'] ) ) { |
| 878 | $payments_nox_profile['suggestions'] = array(); |
| 879 | } |
| 880 | if ( empty( $payments_nox_profile['suggestions'][ $id ] ) ) { |
| 881 | $payments_nox_profile['suggestions'][ $id ] = array(); |
| 882 | } |
| 883 | if ( empty( $payments_nox_profile['suggestions'][ $id ]['attached'] ) ) { |
| 884 | $payments_nox_profile['suggestions'][ $id ]['attached'] = array(); |
| 885 | } |
| 886 | $payments_nox_profile['suggestions'][ $id ]['attached']['timestamp'] = time(); |
| 887 | |
| 888 | // Store the modified profile data. |
| 889 | $result = update_option( Payments::PAYMENTS_NOX_PROFILE_KEY, $payments_nox_profile, false ); |
| 890 | // Since we already check if the suggestion is already attached, we should not get a false result |
| 891 | // for trying to update with the same value. |
| 892 | // False means the update failed and the suggestion is not marked as attached. |
| 893 | if ( false === $result ) { |
| 894 | return false; |
| 895 | } |
| 896 | |
| 897 | // Handle custom attachment logic per-provider. |
| 898 | switch ( $id ) { |
| 899 | case ExtensionSuggestions::PAYPAL_FULL_STACK: |
| 900 | case ExtensionSuggestions::PAYPAL_WALLET: |
| 901 | // Set an option to inform the extension. |
| 902 | update_option( 'woocommerce_paypal_branded', 'payments_settings', false ); |
| 903 | break; |
| 904 | default: |
| 905 | break; |
| 906 | } |
| 907 | |
| 908 | return true; |
| 909 | } |
| 910 | |
| 911 | /** |
| 912 | * Hide a payment extension suggestion. |
| 913 | * |
| 914 | * @param string $id The ID of the payment extension suggestion to hide. |
| 915 | * |
| 916 | * @return bool True if the suggestion was successfully hidden, false otherwise. |
| 917 | * @throws Exception If the suggestion ID is invalid. |
| 918 | */ |
| 919 | public function hide_extension_suggestion( string $id ): bool { |
| 920 | // We may receive a suggestion ID that is actually an order map ID used in the settings page providers list. |
| 921 | // Extract the suggestion ID from the order map ID. |
| 922 | if ( $this->is_suggestion_order_map_id( $id ) ) { |
| 923 | $id = $this->get_suggestion_id_from_order_map_id( $id ); |
| 924 | } |
| 925 | |
| 926 | $suggestion = $this->get_extension_suggestion_by_id( $id ); |
| 927 | if ( is_null( $suggestion ) ) { |
| 928 | throw new Exception( esc_html__( 'Invalid suggestion ID.', 'woocommerce' ) ); |
| 929 | } |
| 930 | |
| 931 | $user_payments_nox_profile = get_user_meta( get_current_user_id(), Payments::PAYMENTS_NOX_PROFILE_KEY, true ); |
| 932 | if ( empty( $user_payments_nox_profile ) ) { |
| 933 | $user_payments_nox_profile = array(); |
| 934 | } else { |
| 935 | $user_payments_nox_profile = maybe_unserialize( $user_payments_nox_profile ); |
| 936 | } |
| 937 | |
| 938 | // Mark the suggestion as hidden. |
| 939 | if ( empty( $user_payments_nox_profile['hidden_suggestions'] ) ) { |
| 940 | $user_payments_nox_profile['hidden_suggestions'] = array(); |
| 941 | } |
| 942 | // Check if it is already hidden. |
| 943 | if ( in_array( $id, array_column( $user_payments_nox_profile['hidden_suggestions'], 'id' ), true ) ) { |
| 944 | return true; |
| 945 | } |
| 946 | $user_payments_nox_profile['hidden_suggestions'][] = array( |
| 947 | 'id' => $id, |
| 948 | 'timestamp' => time(), |
| 949 | ); |
| 950 | |
| 951 | $result = update_user_meta( get_current_user_id(), Payments::PAYMENTS_NOX_PROFILE_KEY, $user_payments_nox_profile ); |
| 952 | // Since we already check if the suggestion is already hidden, we should not get a false result |
| 953 | // for trying to update with the same value. False means the update failed and the suggestion is not hidden. |
| 954 | if ( false === $result ) { |
| 955 | return false; |
| 956 | } |
| 957 | |
| 958 | return true; |
| 959 | } |
| 960 | |
| 961 | /** |
| 962 | * Get the payment extension suggestions categories details. |
| 963 | * |
| 964 | * @return array The payment extension suggestions categories. |
| 965 | */ |
| 966 | public function get_extension_suggestion_categories(): array { |
| 967 | $categories = array(); |
| 968 | $categories[] = array( |
| 969 | 'id' => self::CATEGORY_EXPRESS_CHECKOUT, |
| 970 | '_priority' => 10, |
| 971 | 'title' => esc_html__( 'Wallets & Express checkouts', 'woocommerce' ), |
| 972 | 'description' => esc_html__( 'Allow shoppers to fast-track the checkout process with express options like Apple Pay and Google Pay.', 'woocommerce' ), |
| 973 | ); |
| 974 | $categories[] = array( |
| 975 | 'id' => self::CATEGORY_BNPL, |
| 976 | '_priority' => 20, |
| 977 | 'title' => esc_html__( 'Buy Now, Pay Later', 'woocommerce' ), |
| 978 | 'description' => esc_html__( 'Offer flexible payment options to your shoppers.', 'woocommerce' ), |
| 979 | ); |
| 980 | $categories[] = array( |
| 981 | 'id' => self::CATEGORY_CRYPTO, |
| 982 | '_priority' => 30, |
| 983 | 'title' => esc_html__( 'Crypto Payments', 'woocommerce' ), |
| 984 | 'description' => esc_html__( 'Offer cryptocurrency payment options to your shoppers.', 'woocommerce' ), |
| 985 | ); |
| 986 | $categories[] = array( |
| 987 | 'id' => self::CATEGORY_PSP, |
| 988 | '_priority' => 40, |
| 989 | 'title' => esc_html__( 'Payment Providers', 'woocommerce' ), |
| 990 | 'description' => esc_html__( 'Give your shoppers additional ways to pay.', 'woocommerce' ), |
| 991 | ); |
| 992 | |
| 993 | return $categories; |
| 994 | } |
| 995 | |
| 996 | /** |
| 997 | * Get the payment providers order map. |
| 998 | * |
| 999 | * @return array The payment providers order map. |
| 1000 | */ |
| 1001 | public function get_order_map(): array { |
| 1002 | // This will also handle backwards compatibility. |
| 1003 | return $this->enhance_order_map( get_option( self::PROVIDERS_ORDER_OPTION, array() ) ); |
| 1004 | } |
| 1005 | |
| 1006 | /** |
| 1007 | * Save the payment providers order map. |
| 1008 | * |
| 1009 | * @param array $order_map The order map to save. |
| 1010 | * |
| 1011 | * @return bool True if the payment providers order map was successfully saved, false otherwise. |
| 1012 | */ |
| 1013 | public function save_order_map( array $order_map ): bool { |
| 1014 | return update_option( self::PROVIDERS_ORDER_OPTION, $order_map ); |
| 1015 | } |
| 1016 | |
| 1017 | /** |
| 1018 | * Update the payment providers order map. |
| 1019 | * |
| 1020 | * This has effects both on the Payments settings page and the checkout page |
| 1021 | * since registered payment gateways (enabled or not) are among the providers. |
| 1022 | * |
| 1023 | * @param array $order_map The new order for payment providers. |
| 1024 | * The order map should be an associative array where the keys are the payment provider IDs |
| 1025 | * and the values are the new integer order for the payment provider. |
| 1026 | * This can be a partial list of payment providers and their orders. |
| 1027 | * It can also contain new IDs and their orders. |
| 1028 | * |
| 1029 | * @return bool True if the payment providers ordering was successfully updated, false otherwise. |
| 1030 | */ |
| 1031 | public function update_payment_providers_order_map( array $order_map ): bool { |
| 1032 | $existing_order_map = get_option( self::PROVIDERS_ORDER_OPTION, array() ); |
| 1033 | |
| 1034 | $new_order_map = $this->payment_providers_order_map_apply_mappings( $existing_order_map, $order_map ); |
| 1035 | |
| 1036 | // This will also handle backwards compatibility. |
| 1037 | $new_order_map = $this->enhance_order_map( $new_order_map ); |
| 1038 | |
| 1039 | // Save the new order map to the DB. |
| 1040 | return $this->save_order_map( $new_order_map ); |
| 1041 | } |
| 1042 | |
| 1043 | /** |
| 1044 | * Enhance a payment providers order map. |
| 1045 | * |
| 1046 | * If the payments providers order map is empty, it will be initialized with the current WC payment gateway ordering. |
| 1047 | * If there are missing entries (registered payment gateways, suggestions, offline PMs, etc.), they will be added. |
| 1048 | * Various rules will be enforced (e.g., offline PMs and their relation with the offline PMs group). |
| 1049 | * |
| 1050 | * @param array $order_map The payment providers order map. |
| 1051 | * |
| 1052 | * @return array The updated payment providers order map. |
| 1053 | */ |
| 1054 | public function enhance_order_map( array $order_map ): array { |
| 1055 | // We don't request the display gateways list because we need to get the order of all the registered payment gateways. |
| 1056 | $payment_gateways = $this->get_payment_gateways( false ); |
| 1057 | // Make it a list keyed by the payment gateway ID. |
| 1058 | $payment_gateways = array_combine( |
| 1059 | array_map( |
| 1060 | fn( $gateway ) => $gateway->id, |
| 1061 | $payment_gateways |
| 1062 | ), |
| 1063 | $payment_gateways |
| 1064 | ); |
| 1065 | // Get the payment gateways order map. |
| 1066 | $payment_gateways_order_map = array_flip( array_keys( $payment_gateways ) ); |
| 1067 | // Get the payment gateways to suggestions map. |
| 1068 | // There will be null entries for payment gateways where we couldn't find a suggestion. |
| 1069 | $payment_gateways_to_suggestions_map = array_map( |
| 1070 | fn( $gateway ) => $this->extension_suggestions->get_by_plugin_slug( Utils::normalize_plugin_slug( $this->get_payment_gateway_plugin_slug( $gateway ) ) ), |
| 1071 | $payment_gateways |
| 1072 | ); |
| 1073 | |
| 1074 | /* |
| 1075 | * Initialize the order map with the current ordering. |
| 1076 | */ |
| 1077 | if ( empty( $order_map ) ) { |
| 1078 | $order_map = $payment_gateways_order_map; |
| 1079 | } |
| 1080 | |
| 1081 | $order_map = Utils::order_map_normalize( $order_map ); |
| 1082 | |
| 1083 | $handled_suggestion_ids = array(); |
| 1084 | |
| 1085 | /* |
| 1086 | * Go through the registered gateways and add any missing ones. |
| 1087 | */ |
| 1088 | // Use a map to keep track of the insertion offset for each suggestion ID. |
| 1089 | // We need this so we can place multiple PGs matching a suggestion right after it but maintain their relative order. |
| 1090 | $suggestion_order_map_id_to_offset_map = array(); |
| 1091 | foreach ( $payment_gateways_order_map as $id => $order ) { |
| 1092 | if ( isset( $order_map[ $id ] ) ) { |
| 1093 | continue; |
| 1094 | } |
| 1095 | |
| 1096 | // If there is a suggestion entry matching this payment gateway, |
| 1097 | // we will add the payment gateway right after it so gateways pop-up in place of matching suggestions. |
| 1098 | // We rely on suggestions and matching registered PGs being mutually exclusive in the UI. |
| 1099 | if ( ! empty( $payment_gateways_to_suggestions_map[ $id ] ) ) { |
| 1100 | $suggestion_id = $payment_gateways_to_suggestions_map[ $id ]['id']; |
| 1101 | $suggestion_order_map_id = $this->get_suggestion_order_map_id( $suggestion_id ); |
| 1102 | |
| 1103 | if ( isset( $order_map[ $suggestion_order_map_id ] ) ) { |
| 1104 | // Determine the offset for placing missing PGs after this suggestion. |
| 1105 | if ( ! isset( $suggestion_order_map_id_to_offset_map[ $suggestion_order_map_id ] ) ) { |
| 1106 | $suggestion_order_map_id_to_offset_map[ $suggestion_order_map_id ] = 0; |
| 1107 | } |
| 1108 | $suggestion_order_map_id_to_offset_map[ $suggestion_order_map_id ] += 1; |
| 1109 | |
| 1110 | // Place the missing payment gateway right after the suggestion, |
| 1111 | // with an offset to maintain relative order between multiple PGs matching the same suggestion. |
| 1112 | $order_map = Utils::order_map_place_at_order( |
| 1113 | $order_map, |
| 1114 | $id, |
| 1115 | $order_map[ $suggestion_order_map_id ] + $suggestion_order_map_id_to_offset_map[ $suggestion_order_map_id ] |
| 1116 | ); |
| 1117 | |
| 1118 | // Remember that we handled this suggestion - don't worry about remembering it multiple times. |
| 1119 | $handled_suggestion_ids[] = $suggestion_id; |
| 1120 | continue; |
| 1121 | } |
| 1122 | } |
| 1123 | |
| 1124 | // If the offline PMs group is the last non-offline entry, place above it. |
| 1125 | // Otherwise (custom ordering or no offline group), place at the end. |
| 1126 | $order_map = $this->order_map_add_gateway( $order_map, $id ); |
| 1127 | } |
| 1128 | |
| 1129 | $handled_suggestion_ids = array_unique( $handled_suggestion_ids ); |
| 1130 | |
| 1131 | /* |
| 1132 | * Place not yet handled suggestion entries right before their matching registered payment gateway IDs. |
| 1133 | * This means that registered PGs already in the order map force the suggestions |
| 1134 | * to be placed/moved right before them. We rely on suggestions and registered PGs being mutually exclusive. |
| 1135 | */ |
| 1136 | foreach ( array_keys( $order_map ) as $id ) { |
| 1137 | // If the id is not of a payment gateway or there is no suggestion for this payment gateway, ignore it. |
| 1138 | if ( ! array_key_exists( $id, $payment_gateways_to_suggestions_map ) || |
| 1139 | empty( $payment_gateways_to_suggestions_map[ $id ] ) |
| 1140 | ) { |
| 1141 | continue; |
| 1142 | } |
| 1143 | |
| 1144 | $suggestion = $payment_gateways_to_suggestions_map[ $id ]; |
| 1145 | // If the suggestion was already handled, skip it. |
| 1146 | if ( in_array( $suggestion['id'], $handled_suggestion_ids, true ) ) { |
| 1147 | continue; |
| 1148 | } |
| 1149 | |
| 1150 | // Place the suggestion at the same order as the payment gateway |
| 1151 | // thus ensuring that the suggestion is placed right before the payment gateway. |
| 1152 | $order_map = Utils::order_map_place_at_order( |
| 1153 | $order_map, |
| 1154 | $this->get_suggestion_order_map_id( $suggestion['id'] ), |
| 1155 | $order_map[ $id ] |
| 1156 | ); |
| 1157 | |
| 1158 | // Remember that we've handled this suggestion to avoid adding it multiple times. |
| 1159 | // We only want to attach the suggestion to the first payment gateway that matches the plugin slug. |
| 1160 | $handled_suggestion_ids[] = $suggestion['id']; |
| 1161 | } |
| 1162 | |
| 1163 | // Extract all the registered offline PMs and keep their order values. |
| 1164 | $offline_methods = array_filter( |
| 1165 | $order_map, |
| 1166 | array( $this, 'is_offline_payment_method' ), |
| 1167 | ARRAY_FILTER_USE_KEY |
| 1168 | ); |
| 1169 | if ( ! empty( $offline_methods ) ) { |
| 1170 | /* |
| 1171 | * If the offline PMs group is missing, add it before the last offline PM. |
| 1172 | */ |
| 1173 | if ( ! array_key_exists( self::OFFLINE_METHODS_ORDERING_GROUP, $order_map ) ) { |
| 1174 | $last_offline_method_order = max( $offline_methods ); |
| 1175 | |
| 1176 | $order_map = Utils::order_map_place_at_order( $order_map, self::OFFLINE_METHODS_ORDERING_GROUP, $last_offline_method_order ); |
| 1177 | } |
| 1178 | |
| 1179 | /* |
| 1180 | * Place all the offline PMs right after the offline PMs group entry. |
| 1181 | */ |
| 1182 | $target_order = $order_map[ self::OFFLINE_METHODS_ORDERING_GROUP ] + 1; |
| 1183 | // Sort the offline PMs by their order. |
| 1184 | asort( $offline_methods ); |
| 1185 | foreach ( $offline_methods as $offline_method => $order ) { |
| 1186 | $order_map = Utils::order_map_place_at_order( $order_map, $offline_method, $target_order ); |
| 1187 | ++$target_order; |
| 1188 | } |
| 1189 | } |
| 1190 | |
| 1191 | return Utils::order_map_normalize( $order_map ); |
| 1192 | } |
| 1193 | |
| 1194 | /** |
| 1195 | * Get the ID of the suggestion order map entry. |
| 1196 | * |
| 1197 | * @param string $suggestion_id The ID of the suggestion. |
| 1198 | * |
| 1199 | * @return string The ID of the suggestion order map entry. |
| 1200 | */ |
| 1201 | public function get_suggestion_order_map_id( string $suggestion_id ): string { |
| 1202 | return self::SUGGESTION_ORDERING_PREFIX . $suggestion_id; |
| 1203 | } |
| 1204 | |
| 1205 | /** |
| 1206 | * Check if the ID is a suggestion order map entry ID. |
| 1207 | * |
| 1208 | * @param string $id The ID to check. |
| 1209 | * |
| 1210 | * @return bool True if the ID is a suggestion order map entry ID, false otherwise. |
| 1211 | */ |
| 1212 | public function is_suggestion_order_map_id( string $id ): bool { |
| 1213 | return 0 === strpos( $id, self::SUGGESTION_ORDERING_PREFIX ); |
| 1214 | } |
| 1215 | |
| 1216 | /** |
| 1217 | * Get the ID of the suggestion from the suggestion order map entry ID. |
| 1218 | * |
| 1219 | * @param string $order_map_id The ID of the suggestion order map entry. |
| 1220 | * |
| 1221 | * @return string The ID of the suggestion. |
| 1222 | */ |
| 1223 | public function get_suggestion_id_from_order_map_id( string $order_map_id ): string { |
| 1224 | return str_replace( self::SUGGESTION_ORDERING_PREFIX, '', $order_map_id ); |
| 1225 | } |
| 1226 | |
| 1227 | /** |
| 1228 | * Reset the memoized data. Useful for testing purposes. |
| 1229 | * |
| 1230 | * @internal |
| 1231 | * @return void |
| 1232 | */ |
| 1233 | public function reset_memo(): void { |
| 1234 | $this->payment_gateways_memo = array(); |
| 1235 | $this->payment_gateways_for_display_memo = array(); |
| 1236 | } |
| 1237 | |
| 1238 | /** |
| 1239 | * Handle payment gateways with non-standard registration behavior. |
| 1240 | * |
| 1241 | * @param array $payment_gateways The payment gateways list. |
| 1242 | * |
| 1243 | * @return array The payment gateways list with the necessary adjustments. |
| 1244 | */ |
| 1245 | private function handle_non_standard_registration_for_payment_gateways( array $payment_gateways ): array { |
| 1246 | /* |
| 1247 | * Handle the Mollie gateway's particular behavior: if there are no API keys or no PMs enabled, |
| 1248 | * the extension doesn't register a gateway instance. |
| 1249 | * We will need to register a mock gateway to represent Mollie in the settings page. |
| 1250 | */ |
| 1251 | $payment_gateways = $this->maybe_add_pseudo_mollie_gateway( $payment_gateways ); |
| 1252 | |
| 1253 | return $payment_gateways; |
| 1254 | } |
| 1255 | |
| 1256 | /** |
| 1257 | * Add the pseudo Mollie gateway to the payment gateways list if necessary. |
| 1258 | * |
| 1259 | * @param array $payment_gateways The payment gateways list. |
| 1260 | * |
| 1261 | * @return array The payment gateways list with the pseudo Mollie gateway added if necessary. |
| 1262 | */ |
| 1263 | private function maybe_add_pseudo_mollie_gateway( array $payment_gateways ): array { |
| 1264 | $mollie_provider = $this->get_payment_gateway_provider_instance( 'mollie' ); |
| 1265 | |
| 1266 | // Do nothing if there is a Mollie gateway registered. |
| 1267 | if ( $mollie_provider->is_gateway_registered( $payment_gateways ) ) { |
| 1268 | return $payment_gateways; |
| 1269 | } |
| 1270 | |
| 1271 | // Get the Mollie suggestion and determine if the plugin is active. |
| 1272 | $mollie_suggestion = $this->get_extension_suggestion_by_id( ExtensionSuggestions::MOLLIE ); |
| 1273 | if ( empty( $mollie_suggestion ) ) { |
| 1274 | return $payment_gateways; |
| 1275 | } |
| 1276 | // Do nothing if the plugin is not active. |
| 1277 | if ( self::EXTENSION_ACTIVE !== $mollie_suggestion['plugin']['status'] ) { |
| 1278 | return $payment_gateways; |
| 1279 | } |
| 1280 | |
| 1281 | // Add the pseudo Mollie gateway to the list since the plugin is active but there is no Mollie gateway registered. |
| 1282 | $payment_gateways[] = $mollie_provider->get_pseudo_gateway( $mollie_suggestion ); |
| 1283 | |
| 1284 | return $payment_gateways; |
| 1285 | } |
| 1286 | |
| 1287 | /** |
| 1288 | * Enhance the payment gateway details with additional information from other sources. |
| 1289 | * |
| 1290 | * @param array $gateway_details The gateway details to enhance. |
| 1291 | * @param WC_Payment_Gateway $payment_gateway The payment gateway object. |
| 1292 | * @param string $country_code The country code for which the details are being enhanced. |
| 1293 | * This should be an ISO 3166-1 alpha-2 country code. |
| 1294 | * |
| 1295 | * @return array The enhanced gateway details. |
| 1296 | */ |
| 1297 | private function enhance_payment_gateway_details( array $gateway_details, WC_Payment_Gateway $payment_gateway, string $country_code ): array { |
| 1298 | // We discriminate between offline payment methods and gateways. |
| 1299 | $gateway_details['_type'] = $this->is_offline_payment_method( $payment_gateway->id ) ? self::TYPE_OFFLINE_PM : self::TYPE_GATEWAY; |
| 1300 | |
| 1301 | // Offline payment methods don't have extension suggestions or incentives. |
| 1302 | // Skip the suggestion matching to avoid unnecessary processing. |
| 1303 | if ( self::TYPE_OFFLINE_PM === $gateway_details['_type'] ) { |
| 1304 | return $gateway_details; |
| 1305 | } |
| 1306 | |
| 1307 | $plugin_slug = $gateway_details['plugin']['slug']; |
| 1308 | // The payment gateway plugin might use a non-standard directory name. |
| 1309 | // Try to normalize it to the common slug to avoid false negatives when matching. |
| 1310 | $normalized_plugin_slug = Utils::normalize_plugin_slug( $plugin_slug ); |
| 1311 | |
| 1312 | // If we have a matching suggestion, hoist details from there. |
| 1313 | // The suggestions only know about the normalized (aka official) plugin slug. |
| 1314 | $suggestion = $this->get_extension_suggestion_by_plugin_slug( $normalized_plugin_slug, $country_code ); |
| 1315 | if ( ! is_null( $suggestion ) ) { |
| 1316 | // The title, description, icon, and image from the suggestion take precedence over the ones from the gateway. |
| 1317 | // This is temporary until we update the partner extensions. |
| 1318 | // Do not override the title and description for certain suggestions because theirs are more descriptive |
| 1319 | // (like including the payment method when registering multiple gateways for the same provider). |
| 1320 | if ( ! in_array( |
| 1321 | $suggestion['id'], |
| 1322 | array( |
| 1323 | ExtensionSuggestions::PAYPAL_FULL_STACK, |
| 1324 | ExtensionSuggestions::PAYPAL_WALLET, |
| 1325 | ExtensionSuggestions::MOLLIE, |
| 1326 | ExtensionSuggestions::MONEI, |
| 1327 | ExtensionSuggestions::ANTOM, |
| 1328 | ExtensionSuggestions::MERCADO_PAGO, |
| 1329 | ExtensionSuggestions::AMAZON_PAY, |
| 1330 | ExtensionSuggestions::SQUARE, |
| 1331 | ExtensionSuggestions::PAYONEER, |
| 1332 | ExtensionSuggestions::AIRWALLEX, |
| 1333 | ExtensionSuggestions::COINBASE, // We don't have suggestion details yet. |
| 1334 | ExtensionSuggestions::AUTHORIZE_NET, // We don't have suggestion details yet. |
| 1335 | ExtensionSuggestions::BOLT, // We don't have suggestion details yet. |
| 1336 | ExtensionSuggestions::DEPAY, // We don't have suggestion details yet. |
| 1337 | ExtensionSuggestions::ELAVON, // We don't have suggestion details yet. |
| 1338 | ExtensionSuggestions::FORTISPAY, // We don't have suggestion details yet. |
| 1339 | ExtensionSuggestions::PAYPAL_ZETTLE, // We don't have suggestion details yet. |
| 1340 | ExtensionSuggestions::RAPYD, // We don't have suggestion details yet. |
| 1341 | ExtensionSuggestions::PAYPAL_BRAINTREE, // We don't have suggestion details yet. |
| 1342 | ), |
| 1343 | true |
| 1344 | ) ) { |
| 1345 | if ( ! empty( $suggestion['title'] ) ) { |
| 1346 | $gateway_details['title'] = $suggestion['title']; |
| 1347 | } |
| 1348 | |
| 1349 | if ( ! empty( $suggestion['description'] ) ) { |
| 1350 | $gateway_details['description'] = $suggestion['description']; |
| 1351 | } |
| 1352 | } |
| 1353 | |
| 1354 | if ( ! empty( $suggestion['icon'] ) ) { |
| 1355 | $gateway_details['icon'] = $suggestion['icon']; |
| 1356 | } |
| 1357 | |
| 1358 | if ( ! empty( $suggestion['image'] ) ) { |
| 1359 | $gateway_details['image'] = $suggestion['image']; |
| 1360 | } |
| 1361 | |
| 1362 | if ( empty( $gateway_details['links'] ) && ! empty( $suggestion['links'] ) ) { |
| 1363 | $gateway_details['links'] = $suggestion['links']; |
| 1364 | } |
| 1365 | if ( empty( $gateway_details['tags'] ) && ! empty( $suggestion['tags'] ) ) { |
| 1366 | $gateway_details['tags'] = $suggestion['tags']; |
| 1367 | } |
| 1368 | if ( empty( $gateway_details['plugin'] ) && ! empty( $suggestion['plugin'] ) ) { |
| 1369 | $gateway_details['plugin'] = $suggestion['plugin']; |
| 1370 | } |
| 1371 | if ( empty( $gateway_details['_incentive'] ) && ! empty( $suggestion['_incentive'] ) ) { |
| 1372 | $gateway_details['_incentive'] = $suggestion['_incentive']; |
| 1373 | } |
| 1374 | |
| 1375 | // Attach the suggestion ID to the gateway details so we can reference it with precision. |
| 1376 | $gateway_details['_suggestion_id'] = $suggestion['id']; |
| 1377 | } |
| 1378 | |
| 1379 | // Get the gateway's corresponding plugin details. |
| 1380 | $plugin_data = $this->proxy->call_static( PluginsHelper::class, 'get_plugin_data', $plugin_slug ); |
| 1381 | if ( ! empty( $plugin_data ) ) { |
| 1382 | // If there are no links, try to get them from the plugin data. |
| 1383 | if ( empty( $gateway_details['links'] ) ) { |
| 1384 | if ( is_array( $plugin_data ) && ! empty( $plugin_data['PluginURI'] ) ) { |
| 1385 | $gateway_details['links'] = array( |
| 1386 | array( |
| 1387 | '_type' => self::LINK_TYPE_ABOUT, |
| 1388 | 'url' => esc_url( $plugin_data['PluginURI'] ), |
| 1389 | ), |
| 1390 | ); |
| 1391 | } elseif ( ! empty( $gateway_details['plugin']['_type'] ) && |
| 1392 | ExtensionSuggestions::PLUGIN_TYPE_WPORG === $gateway_details['plugin']['_type'] ) { |
| 1393 | |
| 1394 | // Fallback to constructing the WPORG plugin URI from the normalized plugin slug. |
| 1395 | $gateway_details['links'] = array( |
| 1396 | array( |
| 1397 | '_type' => self::LINK_TYPE_ABOUT, |
| 1398 | 'url' => 'https://wordpress.org/plugins/' . $normalized_plugin_slug, |
| 1399 | ), |
| 1400 | ); |
| 1401 | } |
| 1402 | } |
| 1403 | } |
| 1404 | |
| 1405 | return $gateway_details; |
| 1406 | } |
| 1407 | |
| 1408 | /** |
| 1409 | * Check if the store has any enabled ecommerce gateways. |
| 1410 | * |
| 1411 | * We exclude offline payment methods from this check. |
| 1412 | * |
| 1413 | * @return bool True if the store has any enabled ecommerce gateways, false otherwise. |
| 1414 | */ |
| 1415 | private function has_enabled_ecommerce_gateways(): bool { |
| 1416 | $gateways = $this->get_payment_gateways( false ); // We want the raw gateways list. |
| 1417 | $enabled_gateways = array_filter( |
| 1418 | $gateways, |
| 1419 | function ( $gateway ) { |
| 1420 | // Filter out offline gateways. |
| 1421 | return 'yes' === $gateway->enabled && ! $this->is_offline_payment_method( $gateway->id ); |
| 1422 | } |
| 1423 | ); |
| 1424 | |
| 1425 | return ! empty( $enabled_gateways ); |
| 1426 | } |
| 1427 | |
| 1428 | /** |
| 1429 | * Enhance a payment extension suggestion with additional information. |
| 1430 | * |
| 1431 | * @param array $extension_suggestion The extension suggestion. |
| 1432 | * |
| 1433 | * @return array The enhanced payment extension suggestion. |
| 1434 | */ |
| 1435 | private function enhance_extension_suggestion( array $extension_suggestion ): array { |
| 1436 | // Determine the category of the extension. |
| 1437 | switch ( $extension_suggestion['_type'] ) { |
| 1438 | case ExtensionSuggestions::TYPE_PSP: |
| 1439 | $extension_suggestion['category'] = self::CATEGORY_PSP; |
| 1440 | break; |
| 1441 | case ExtensionSuggestions::TYPE_EXPRESS_CHECKOUT: |
| 1442 | $extension_suggestion['category'] = self::CATEGORY_EXPRESS_CHECKOUT; |
| 1443 | break; |
| 1444 | case ExtensionSuggestions::TYPE_BNPL: |
| 1445 | $extension_suggestion['category'] = self::CATEGORY_BNPL; |
| 1446 | break; |
| 1447 | case ExtensionSuggestions::TYPE_CRYPTO: |
| 1448 | $extension_suggestion['category'] = self::CATEGORY_CRYPTO; |
| 1449 | break; |
| 1450 | default: |
| 1451 | $extension_suggestion['category'] = ''; |
| 1452 | break; |
| 1453 | } |
| 1454 | |
| 1455 | // Determine the PES's plugin status. |
| 1456 | // Default to not installed. |
| 1457 | $extension_suggestion['plugin']['status'] = self::EXTENSION_NOT_INSTALLED; |
| 1458 | // Put in the default plugin file. |
| 1459 | $extension_suggestion['plugin']['file'] = ''; |
| 1460 | if ( ! empty( $extension_suggestion['plugin']['slug'] ) ) { |
| 1461 | // This is a best-effort approach, as the plugin might be sitting under a directory (slug) that we can't handle. |
| 1462 | // Always try the official plugin slug first, then the testing variations. |
| 1463 | $plugin_slug_variations = Utils::generate_testing_plugin_slugs( $extension_suggestion['plugin']['slug'], true ); |
| 1464 | // Favor active plugins by checking the entire variations list for active plugins first. |
| 1465 | // This way we handle cases where there are multiple variations installed and one is active. |
| 1466 | $found = false; |
| 1467 | foreach ( $plugin_slug_variations as $plugin_slug ) { |
| 1468 | if ( $this->proxy->call_static( PluginsHelper::class, 'is_plugin_active', $plugin_slug ) ) { |
| 1469 | $found = true; |
| 1470 | $extension_suggestion['plugin']['status'] = self::EXTENSION_ACTIVE; |
| 1471 | // Make sure we put in the actual slug and file path that we found. |
| 1472 | $extension_suggestion['plugin']['slug'] = $plugin_slug; |
| 1473 | $extension_suggestion['plugin']['file'] = $this->proxy->call_static( PluginsHelper::class, 'get_plugin_path_from_slug', $plugin_slug ); |
| 1474 | // Sanity check. |
| 1475 | if ( ! is_string( $extension_suggestion['plugin']['file'] ) ) { |
| 1476 | $extension_suggestion['plugin']['file'] = ''; |
| 1477 | break; |
| 1478 | } |
| 1479 | // Remove the .php extension from the file path. The WP API expects it without it. |
| 1480 | $extension_suggestion['plugin']['file'] = Utils::trim_php_file_extension( $extension_suggestion['plugin']['file'] ); |
| 1481 | break; |
| 1482 | } |
| 1483 | } |
| 1484 | if ( ! $found ) { |
| 1485 | foreach ( $plugin_slug_variations as $plugin_slug ) { |
| 1486 | if ( $this->proxy->call_static( PluginsHelper::class, 'is_plugin_installed', $plugin_slug ) ) { |
| 1487 | $extension_suggestion['plugin']['status'] = self::EXTENSION_INSTALLED; |
| 1488 | // Make sure we put in the actual slug and file path that we found. |
| 1489 | $extension_suggestion['plugin']['slug'] = $plugin_slug; |
| 1490 | $extension_suggestion['plugin']['file'] = $this->proxy->call_static( PluginsHelper::class, 'get_plugin_path_from_slug', $plugin_slug ); |
| 1491 | // Sanity check. |
| 1492 | if ( ! is_string( $extension_suggestion['plugin']['file'] ) ) { |
| 1493 | $extension_suggestion['plugin']['file'] = ''; |
| 1494 | break; |
| 1495 | } |
| 1496 | // Remove the .php extension from the file path. The WP API expects it without it. |
| 1497 | $extension_suggestion['plugin']['file'] = Utils::trim_php_file_extension( $extension_suggestion['plugin']['file'] ); |
| 1498 | break; |
| 1499 | } |
| 1500 | } |
| 1501 | } |
| 1502 | } |
| 1503 | |
| 1504 | // Finally, allow the extension suggestion's matching provider to add further details. |
| 1505 | $gateway_provider = $this->get_payment_extension_suggestion_provider_instance( $extension_suggestion['id'] ); |
| 1506 | $extension_suggestion = $gateway_provider->enhance_extension_suggestion( $extension_suggestion ); |
| 1507 | |
| 1508 | return $extension_suggestion; |
| 1509 | } |
| 1510 | |
| 1511 | /** |
| 1512 | * Check if a payment extension suggestion has been hidden by the user. |
| 1513 | * |
| 1514 | * @param array $extension The extension suggestion. |
| 1515 | * |
| 1516 | * @return bool True if the extension suggestion is hidden, false otherwise. |
| 1517 | */ |
| 1518 | private function is_payment_extension_suggestion_hidden( array $extension ): bool { |
| 1519 | $user_payments_nox_profile = get_user_meta( get_current_user_id(), Payments::PAYMENTS_NOX_PROFILE_KEY, true ); |
| 1520 | if ( empty( $user_payments_nox_profile ) ) { |
| 1521 | return false; |
| 1522 | } |
| 1523 | $user_payments_nox_profile = maybe_unserialize( $user_payments_nox_profile ); |
| 1524 | |
| 1525 | if ( empty( $user_payments_nox_profile['hidden_suggestions'] ) ) { |
| 1526 | return false; |
| 1527 | } |
| 1528 | |
| 1529 | return in_array( $extension['id'], array_column( $user_payments_nox_profile['hidden_suggestions'], 'id' ), true ); |
| 1530 | } |
| 1531 | |
| 1532 | /** |
| 1533 | * Apply order mappings to a base payment providers order map. |
| 1534 | * |
| 1535 | * @param array $base_map The base order map. |
| 1536 | * @param array $new_mappings The order mappings to apply. |
| 1537 | * This can be a full or partial list of the base one, |
| 1538 | * but it can also contain (only) new provider IDs and their orders. |
| 1539 | * |
| 1540 | * @return array The updated base order map, normalized. |
| 1541 | */ |
| 1542 | private function payment_providers_order_map_apply_mappings( array $base_map, array $new_mappings ): array { |
| 1543 | // Sanity checks. |
| 1544 | // Remove any null or non-integer values. |
| 1545 | $new_mappings = array_filter( $new_mappings, 'is_int' ); |
| 1546 | if ( empty( $new_mappings ) ) { |
| 1547 | $new_mappings = array(); |
| 1548 | } |
| 1549 | |
| 1550 | // If we have no existing order map or |
| 1551 | // both the base and the new map have the same length and keys, we can simply use the new map. |
| 1552 | if ( empty( $base_map ) || |
| 1553 | ( count( $base_map ) === count( $new_mappings ) && |
| 1554 | empty( array_diff( array_keys( $base_map ), array_keys( $new_mappings ) ) ) ) |
| 1555 | ) { |
| 1556 | $new_order_map = $new_mappings; |
| 1557 | } else { |
| 1558 | // If we are dealing with ONLY offline PMs updates (for all that are registered) and their group is present, |
| 1559 | // normalize the new order map to keep behavior as intended (i.e., reorder only inside the offline PMs list). |
| 1560 | $offline_pms = $this->get_offline_payment_methods_gateways(); |
| 1561 | // Make it a list keyed by the payment gateway ID. |
| 1562 | $offline_pms = array_combine( |
| 1563 | array_map( |
| 1564 | fn( $gateway ) => $gateway->id, |
| 1565 | $offline_pms |
| 1566 | ), |
| 1567 | $offline_pms |
| 1568 | ); |
| 1569 | if ( |
| 1570 | isset( $base_map[ self::OFFLINE_METHODS_ORDERING_GROUP ] ) && |
| 1571 | count( $new_mappings ) === count( $offline_pms ) && |
| 1572 | empty( array_diff( array_keys( $new_mappings ), array_keys( $offline_pms ) ) ) |
| 1573 | ) { |
| 1574 | |
| 1575 | $new_mappings = Utils::order_map_change_min_order( $new_mappings, $base_map[ self::OFFLINE_METHODS_ORDERING_GROUP ] + 1 ); |
| 1576 | } |
| 1577 | |
| 1578 | $new_order_map = Utils::order_map_apply_mappings( $base_map, $new_mappings ); |
| 1579 | } |
| 1580 | |
| 1581 | return Utils::order_map_normalize( $new_order_map ); |
| 1582 | } |
| 1583 | |
| 1584 | /** |
| 1585 | * Group payment gateways by their plugin extension filename. |
| 1586 | * |
| 1587 | * @param WC_Payment_Gateway[] $gateways The list of payment gateway instances to group. |
| 1588 | * @param string $country_code Optional. The country code for which the gateways are being generated. |
| 1589 | * This should be an ISO 3166-1 alpha-2 country code. |
| 1590 | * |
| 1591 | * @return array The grouped payment gateway instances, keyed by the plugin file. |
| 1592 | * Each group contains an array of payment gateway instances that belong to the same plugin. |
| 1593 | * If a payment gateway does not have a corresponding plugin file, |
| 1594 | * it will be grouped under the 'unknown_extension' key. |
| 1595 | */ |
| 1596 | private function group_gateways_by_extension( array $gateways, string $country_code = '' ): array { |
| 1597 | $grouped = array( |
| 1598 | // This is the group for gateways that we don't know how to group by extension. |
| 1599 | // It can be used for gateways that are not registered by a WP plugin. |
| 1600 | 'unknown_extension' => array(), |
| 1601 | ); |
| 1602 | |
| 1603 | foreach ( $gateways as $gateway ) { |
| 1604 | // Get the payment gateway details, but use a dummy gateway order since it is inconsequential here. |
| 1605 | $gateway_details = $this->get_payment_gateway_details( $gateway, 0, $country_code ); |
| 1606 | // If we don't have the necessary plugin details, put it in the unknown group. |
| 1607 | if ( empty( $gateway_details ) || ! isset( $gateway_details['plugin'] ) || empty( $gateway_details['plugin']['file'] ) ) { |
| 1608 | $grouped['unknown_extension'][] = $gateway; |
| 1609 | continue; |
| 1610 | } |
| 1611 | |
| 1612 | if ( empty( $grouped[ $gateway_details['plugin']['file'] ] ) ) { |
| 1613 | $grouped[ $gateway_details['plugin']['file'] ] = array(); |
| 1614 | } |
| 1615 | |
| 1616 | $grouped[ $gateway_details['plugin']['file'] ][] = $gateway; |
| 1617 | } |
| 1618 | |
| 1619 | return $grouped; |
| 1620 | } |
| 1621 | } |
| 1622 |