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 / PaymentsProviders.php
woocommerce / src / Internal / Admin / Settings Last commit date
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