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