t the normalized (aka official) plugin slug. $suggestion = $this->get_extension_suggestion_by_plugin_slug( $normalized_plugin_slug, $country_code ); if ( ! is_null( $suggestion ) ) { // The title, description, icon, and image from the suggestion take precedence over the ones from the gateway. // This is temporary until we update the partner extensions. // Do not override the title and description for certain suggestions because theirs are more descriptive // (like including the payment method when registering multiple gateways for the same provider). if ( ! in_array( $suggestion['id'], array( ExtensionSuggestions::PAYPAL_FULL_STACK, ExtensionSuggestions::PAYPAL_WALLET, ExtensionSuggestions::MOLLIE, ExtensionSuggestions::MONEI, ExtensionSuggestions::ANTOM, ExtensionSuggestions::MERCADO_PAGO, ExtensionSuggestions::AMAZON_PAY, ExtensionSuggestions::SQUARE, ExtensionSuggestions::PAYONEER, ExtensionSuggestions::AIRWALLEX, ExtensionSuggestions::COINBASE, // We don't have suggestion details yet. ExtensionSuggestions::AUTHORIZE_NET, // We don't have suggestion details yet. ExtensionSuggestions::BOLT, // We don't have suggestion details yet. ExtensionSuggestions::DEPAY, // We don't have suggestion details yet. ExtensionSuggestions::ELAVON, // We don't have suggestion details yet. ExtensionSuggestions::FORTISPAY, // We don't have suggestion details yet. ExtensionSuggestions::PAYPAL_ZETTLE, // We don't have suggestion details yet. ExtensionSuggestions::RAPYD, // We don't have suggestion details yet. ExtensionSuggestions::PAYPAL_BRAINTREE, // We don't have suggestion details yet. ), true ) ) { if ( ! empty( $suggestion['title'] ) ) { $gateway_details['title'] = $suggestion['title']; } if ( ! empty( $suggestion['description'] ) ) { $gateway_details['description'] = $suggestion['description']; } } if ( ! empty( $suggestion['icon'] ) ) { $gateway_details['icon'] = $suggestion['icon']; } if ( ! empty( $suggestion['image'] ) ) { $gateway_details['image'] = $suggestion['image']; } if ( empty( $gateway_details['links'] ) && ! empty( $suggestion['links'] ) ) { $gateway_details['links'] = $suggestion['links']; } if ( empty( $gateway_details['tags'] ) && ! empty( $suggestion['tags'] ) ) { $gateway_details['tags'] = $suggestion['tags']; } if ( empty( $gateway_details['plugin'] ) && ! empty( $suggestion['plugin'] ) ) { $gateway_details['plugin'] = $suggestion['plugin']; } if ( empty( $gateway_details['_incentive'] ) && ! empty( $suggestion['_incentive'] ) ) { $gateway_details['_incentive'] = $suggestion['_incentive']; } // Attach the suggestion ID to the gateway details so we can reference it with precision. $gateway_details['_suggestion_id'] = $suggestion['id']; } // Get the gateway's corresponding plugin details. $plugin_data = $this->proxy->call_static( PluginsHelper::class, 'get_plugin_data', $plugin_slug ); if ( ! empty( $plugin_data ) ) { // If there are no links, try to get them from the plugin data. if ( empty( $gateway_details['links'] ) ) { if ( is_array( $plugin_data ) && ! empty( $plugin_data['PluginURI'] ) ) { $gateway_details['links'] = array( array( '_type' => self::LINK_TYPE_ABOUT, 'url' => esc_url( $plugin_data['PluginURI'] ), ), ); } elseif ( ! empty( $gateway_details['plugin']['_type'] ) && ExtensionSuggestions::PLUGIN_TYPE_WPORG === $gateway_details['plugin']['_type'] ) { // Fallback to constructing the WPORG plugin URI from the normalized plugin slug. $gateway_details['links'] = array( array( '_type' => self::LINK_TYPE_ABOUT, 'url' => 'https://wordpress.org/plugins/' . $normalized_plugin_slug, ), ); } } } return $gateway_details; } /** * Check if the store has any enabled ecommerce gateways. * * We exclude offline payment methods from this check. * * @return bool True if the store has any enabled ecommerce gateways, false otherwise. */ private function has_enabled_ecommerce_gateways(): bool { $gateways = $this->get_payment_gateways( false ); // We want the raw gateways list. $enabled_gateways = array_filter( $gateways, function ( $gateway ) { // Filter out offline gateways. return 'yes' === $gateway->enabled && ! $this->is_offline_payment_method( $gateway->id ); } ); return ! empty( $enabled_gateways ); } /** * Enhance a payment extension suggestion with additional information. * * @param array $extension_suggestion The extension suggestion. * * @return array The enhanced payment extension suggestion. */ private function enhance_extension_suggestion( array $extension_suggestion ): array { // Determine the category of the extension. switch ( $extension_suggestion['_type'] ) { case ExtensionSuggestions::TYPE_PSP: $extension_suggestion['category'] = self::CATEGORY_PSP; break; case ExtensionSuggestions::TYPE_EXPRESS_CHECKOUT: $extension_suggestion['category'] = self::CATEGORY_EXPRESS_CHECKOUT; break; case ExtensionSuggestions::TYPE_BNPL: $extension_suggestion['category'] = self::CATEGORY_BNPL; break; case ExtensionSuggestions::TYPE_CRYPTO: $extension_suggestion['category'] = self::CATEGORY_CRYPTO; break; default: $extension_suggestion['category'] = ''; break; } // Determine the PES's plugin status. // Default to not installed. $extension_suggestion['plugin']['status'] = self::EXTENSION_NOT_INSTALLED; // Put in the default plugin file. $extension_suggestion['plugin']['file'] = ''; if ( ! empty( $extension_suggestion['plugin']['slug'] ) ) { // This is a best-effort approach, as the plugin might be sitting under a directory (slug) that we can't handle. // Always try the official plugin slug first, then the testing variations. $plugin_slug_variations = Utils::generate_testing_plugin_slugs( $extension_suggestion['plugin']['slug'], true ); // Favor active plugins by checking the entire variations list for active plugins first. // This way we handle cases where there are multiple variations installed and one is active. $found = false; foreach ( $plugin_slug_variations as $plugin_slug ) { if ( $this->proxy->call_static( PluginsHelper::class, 'is_plugin_active', $plugin_slug ) ) { $found = true; $extension_suggestion['plugin']['status'] = self::EXTENSION_ACTIVE; // Make sure we put in the actual slug and file path that we found. $extension_suggestion['plugin']['slug'] = $plugin_slug; $extension_suggestion['plugin']['file'] = $this->proxy->call_static( PluginsHelper::class, 'get_plugin_path_from_slug', $plugin_slug ); // Sanity check. if ( ! is_string( $extension_suggestion['plugin']['file'] ) ) { $extension_suggestion['plugin']['file'] = ''; break; } // Remove the .php extension from the file path. The WP API expects it without it. $extension_suggestion['plugin']['file'] = Utils::trim_php_file_extension( $extension_suggestion['plugin']['file'] ); break; } } if ( ! $found ) { foreach ( $plugin_slug_variations as $plugin_slug ) { if ( $this->proxy->call_static( PluginsHelper::class, 'is_plugin_installed', $plugin_slug ) ) { $extension_suggestion['plugin']['status'] = self::EXTENSION_INSTALLED; // Make sure we put in the actual slug and file path that we found. $extension_suggestion['plugin']['slug'] = $plugin_slug; $extension_suggestion['plugin']['file'] = $this->proxy->call_static( PluginsHelper::class, 'get_plugin_path_from_slug', $plugin_slug ); // Sanity check. if ( ! is_string( $extension_suggestion['plugin']['file'] ) ) { $extension_suggestion['plugin']['file'] = ''; break; } // Remove the .php extension from the file path. The WP API expects it without it. $extension_suggestion['plugin']['file'] = Utils::trim_php_file_extension( $extension_suggestion['plugin']['file'] ); break; } } } } // Finally, allow the extension suggestion's matching provider to add further details. $gateway_provider = $this->get_payment_extension_suggestion_provider_instance( $extension_suggestion['id'] ); $extension_suggestion = $gateway_provider->enhance_extension_suggestion( $extension_suggestion ); return $extension_suggestion; } /** * Check if a payment extension suggestion has been hidden by the user. * * @param array $extension The extension suggestion. * * @return bool True if the extension suggestion is hidden, false otherwise. */ private function is_payment_extension_suggestion_hidden( array $extension ): bool { $user_payments_nox_profile = get_user_meta( get_current_user_id(), Payments::PAYMENTS_NOX_PROFILE_KEY, true ); if ( empty( $user_payments_nox_profile ) ) { return false; } $user_payments_nox_profile = maybe_unserialize( $user_payments_nox_profile ); if ( empty( $user_payments_nox_profile['hidden_suggestions'] ) ) { return false; } return in_array( $extension['id'], array_column( $user_payments_nox_profile['hidden_suggestions'], 'id' ), true ); } /** * Apply order mappings to a base payment providers order map. * * @param array $base_map The base order map. * @param array $new_mappings The order mappings to apply. * This can be a full or partial list of the base one, * but it can also contain (only) new provider IDs and their orders. * * @return array The updated base order map, normalized. */ private function payment_providers_order_map_apply_mappings( array $base_map, array $new_mappings ): array { // Sanity checks. // Remove any null or non-integer values. $new_mappings = array_filter( $new_mappings, 'is_int' ); if ( empty( $new_mappings ) ) { $new_mappings = array(); } // If we have no existing order map or // both the base and the new map have the same length and keys, we can simply use the new map. if ( empty( $base_map ) || ( count( $base_map ) === count( $new_mappings ) && empty( array_diff( array_keys( $base_map ), array_keys( $new_mappings ) ) ) ) ) { $new_order_map = $new_mappings; } else { // If we are dealing with ONLY offline PMs updates (for all that are registered) and their group is present, // normalize the new order map to keep behavior as intended (i.e., reorder only inside the offline PMs list). $offline_pms = $this->get_offline_payment_methods_gateways(); // Make it a list keyed by the payment gateway ID. $offline_pms = array_combine( array_map( fn( $gateway ) => $gateway->id, $offline_pms ), $offline_pms ); if ( isset( $base_map[ self::OFFLINE_METHODS_ORDERING_GROUP ] ) && count( $new_mappings ) === count( $offline_pms ) && empty( array_diff( array_keys( $new_mappings ), array_keys( $offline_pms ) ) ) ) { $new_mappings = Utils::order_map_change_min_order( $new_mappings, $base_map[ self::OFFLINE_METHODS_ORDERING_GROUP ] + 1 ); } $new_order_map = Utils::order_map_apply_mappings( $base_map, $new_mappings ); } return Utils::order_map_normalize( $new_order_map ); } /** * Group payment gateways by their plugin extension filename. * * @param WC_Payment_Gateway[] $gateways The list of payment gateway instances to group. * @param string $country_code Optional. The country code for which the gateways are being generated. * This should be an ISO 3166-1 alpha-2 country code. * * @return array The grouped payment gateway instances, keyed by the plugin file. * Each group contains an array of payment gateway instances that belong to the same plugin. * If a payment gateway does not have a corresponding plugin file, * it will be grouped under the 'unknown_extension' key. */ private function group_gateways_by_extension( array $gateways, string $country_code = '' ): array { $grouped = array( // This is the group for gateways that we don't know how to group by extension. // It can be used for gateways that are not registered by a WP plugin. 'unknown_extension' => array(), ); foreach ( $gateways as $gateway ) { // Get the payment gateway details, but use a dummy gateway order since it is inconsequential here. $gateway_details = $this->get_payment_gateway_details( $gateway, 0, $country_code ); // If we don't have the necessary plugin details, put it in the unknown group. if ( empty( $gateway_details ) || ! isset( $gateway_details['plugin'] ) || empty( $gateway_details['plugin']['file'] ) ) { $grouped['unknown_extension'][] = $gateway; continue; } if ( empty( $grouped[ $gateway_details['plugin']['file'] ] ) ) { $grouped[ $gateway_details['plugin']['file'] ] = array(); } $grouped[ $gateway_details['plugin']['file'] ][] = $gateway; } return $grouped; } }