diff --git a/assets/css/admin/facebook-for-woocommerce-connection.css b/assets/css/admin/facebook-for-woocommerce-connection.css index c054b0c36..03a1016b8 100644 --- a/assets/css/admin/facebook-for-woocommerce-connection.css +++ b/assets/css/admin/facebook-for-woocommerce-connection.css @@ -76,3 +76,12 @@ #wc-facebook-connection-box .uninstall { vertical-align: middle; } + +#facebook-commerce-iframe { + width: 100%; + min-height: calc(100vh - 200px); +} + +.woocommerce-embed-page #wpbody-content { + padding-bottom: 0; +} \ No newline at end of file diff --git a/class-wc-facebookcommerce.php b/class-wc-facebookcommerce.php index 8b436ea64..d9d793f09 100644 --- a/class-wc-facebookcommerce.php +++ b/class-wc-facebookcommerce.php @@ -220,6 +220,7 @@ public function init() { } $this->connection_handler = new WooCommerce\Facebook\Handlers\Connection( $this ); + new WooCommerce\Facebook\Handlers\MetaExtension(); $this->webhook_handler = new WooCommerce\Facebook\Handlers\WebHook( $this ); $this->tracker = new WooCommerce\Facebook\Utilities\Tracker(); diff --git a/facebook-commerce.php b/facebook-commerce.php index fefec06e7..0e5ce5118 100644 --- a/facebook-commerce.php +++ b/facebook-commerce.php @@ -1,5 +1,6 @@ Facebook for WooCommerce
'; public const FB_SYNC_IN_PROGRESS = 'fb_sync_in_progress'; - public const FB_SYNC_REMAINING = 'fb_sync_remaining'; - public const FB_SYNC_TIMEOUT = 30; - public const FB_PRIORITY_MID = 9; + public const FB_SYNC_REMAINING = 'fb_sync_remaining'; + public const FB_SYNC_TIMEOUT = 30; + public const FB_PRIORITY_MID = 9; /** * Facebook exception test mode switch. @@ -183,6 +184,7 @@ class WC_Facebookcommerce_Integration extends WC_Integration { * Init and hook in the integration. * * @param WC_Facebookcommerce $facebook_for_woocommerce + * * @return void */ public function __construct( WC_Facebookcommerce $facebook_for_woocommerce ) { @@ -235,13 +237,13 @@ public function __construct( WC_Facebookcommerce $facebook_for_woocommerce ) { // Display an info banner for eligible pixel and user. if ( $this->get_external_merchant_settings_id() - && $this->get_facebook_pixel_id() - && $this->get_pixel_install_time() ) { + && $this->get_facebook_pixel_id() + && $this->get_pixel_install_time() ) { $should_query_tip = - WC_Facebookcommerce_Utils::check_time_cap( - get_option( 'fb_info_banner_last_query_time', '' ), - self::FB_TIP_QUERY - ); + WC_Facebookcommerce_Utils::check_time_cap( + get_option( 'fb_info_banner_last_query_time', '' ), + self::FB_TIP_QUERY + ); $last_tip_info = WC_Facebookcommerce_Utils::get_cached_best_tip(); if ( $should_query_tip || $last_tip_info ) { @@ -372,6 +374,7 @@ public function __construct( WC_Facebookcommerce $facebook_for_woocommerce ) { * __get method for backward compatibility. * * @param string $key property name + * * @return mixed * @since 3.0.32 */ @@ -380,6 +383,7 @@ public function __get( $key ) { if ( in_array( $key, array( 'events_tracker', 'background_processor' ), true ) ) { /* translators: %s property name. */ _doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The %s property is private and should not be accessed outside its class.', 'facebook-for-woocommerce' ), esc_html( $key ) ), '3.0.32' ); + return $this->$key; } @@ -406,7 +410,7 @@ public function init_pixel() { if ( WC_Facebookcommerce_Utils::is_valid_id( $settings_pixel_id ) && ( ! WC_Facebookcommerce_Utils::is_valid_id( $pixel_id ) || - $pixel_id != $settings_pixel_id + $pixel_id != $settings_pixel_id ) ) { WC_Facebookcommerce_Pixel::set_pixel_id( $settings_pixel_id ); @@ -426,15 +430,16 @@ public function init_pixel() { return true; } + return false; } /** * Returns the Automatic advanced matching of this pixel * + * @return AAMSettings * @since 2.0.3 * - * @return AAMSettings */ private function load_aam_settings_of_pixel() { $installed_pixel = $this->get_facebook_pixel_id(); @@ -467,6 +472,7 @@ private function load_aam_settings_of_pixel() { set_transient( $config_key, strval( $aam_settings ), $refresh_interval ); } } + return $aam_settings; } @@ -532,11 +538,12 @@ public function ajax_fb_background_check_queue() { /** * Gets a list of Product Item IDs indexed by the ID of the variation. * - * @since 2.0.0 - * * @param WC_Facebook_Product|WC_Product $product product - * @param string $product_group_id product group ID + * @param string $product_group_id product group ID + * * @return array + * @since 2.0.0 + * */ public function get_variation_product_item_ids( $product, $product_group_id ) { $product_item_ids_by_variation_id = []; @@ -548,8 +555,8 @@ public function get_variation_product_item_ids( $product, $product_group_id ) { if ( $product_item_id = $variation->get_meta( self::FB_PRODUCT_ITEM_ID ) ) { $product_item_ids_by_variation_id[ $variation_id ] = $product_item_id; } else { - $retailer_id = WC_Facebookcommerce_Utils::get_fb_retailer_id( $variation ); - $missing_product_item_ids[ $retailer_id ] = $variation; + $retailer_id = WC_Facebookcommerce_Utils::get_fb_retailer_id( $variation ); + $missing_product_item_ids[ $retailer_id ] = $variation; $product_item_ids_by_variation_id[ $variation_id ] = null; } } @@ -580,6 +587,7 @@ public function get_variation_product_item_ids( $product, $product_group_id ) { * ) * * @param string $product_group_id product group ID + * * @return array a map of ( `retailer id` -> `id` ) pairs. */ private function find_variation_product_item_ids( string $product_group_id ): array { @@ -594,6 +602,7 @@ private function find_variation_product_item_ids( string $product_group_id ): ar $message = sprintf( 'There was an error trying to find the IDs for Product Items in the Product Group %s: %s', $product_group_id, $e->getMessage() ); WC_Facebookcommerce_Utils::log( $message ); } + return $product_item_ids; } @@ -604,6 +613,7 @@ private function find_variation_product_item_ids( string $product_group_id ): ar */ public function get_product_count() { $product_counts = wp_count_posts( 'product' ); + return $product_counts->publish; } @@ -706,7 +716,7 @@ public function load_assets() { diaSettingId: 'get_external_merchant_settings_id() ? esc_js( $this->get_external_merchant_settings_id() ) : ''; ?>', store: { baseUrl: window.location.protocol + '//' + window.location.host, - baseCurrency:'', + baseCurrency: '', timezoneId: '', storeName: '', version: 'version ); ?>', @@ -750,28 +760,30 @@ public function load_assets() { /** * Gets the IDs of products marked for deletion from Facebook when removed from Sync. * - * @internal - * + * @return array * @since 2.3.0 * - * @return array + * @internal + * */ private function get_removed_from_sync_products_to_delete() { $posted_products = Helper::get_posted_value( WC_Facebook_Product::FB_REMOVE_FROM_SYNC ); if ( empty( $posted_products ) ) { return []; } + return array_map( 'absint', explode( ',', $posted_products ) ); } /** * Checks the product type and calls the corresponding on publish method. * - * @internal + * @param int $wp_id post ID * * @since 1.10.0 * - * @param int $wp_id post ID + * @internal + * */ public function on_product_save( int $wp_id ) { $product = wc_get_product( $wp_id ); @@ -843,11 +855,11 @@ public function on_product_save( int $wp_id ) { } /** - * Saves the submitted Facebook settings for a variable product. - * - * - * @param \WC_Product $product The variable product object. - */ + * Saves the submitted Facebook settings for a variable product. + * + * + * @param \WC_Product $product The variable product object. + */ private function save_variable_product_settings( WC_Product $product ) { $woo_product = new WC_Facebook_Product( $product->get_id() ); if ( isset( $_POST[ WC_Facebook_Product::FB_VARIABLE_BRAND ] ) ) { @@ -858,9 +870,10 @@ private function save_variable_product_settings( WC_Product $product ) { /** * Saves the submitted Facebook settings for a product. * + * @param \WC_Product $product the product object + * * @since 1.10.0 * - * @param \WC_Product $product the product object */ private function save_product_settings( WC_Product $product ) { $woo_product = new WC_Facebook_Product( $product->get_id() ); @@ -931,11 +944,12 @@ public function on_product_delete( int $product_id ) { /** * Deletes Facebook product. * - * @internal + * @param \WC_Product $product WooCommerce product object * * @since 2.3.0 * - * @param \WC_Product $product WooCommerce product object + * @internal + * */ public function delete_fb_product( $product ) { @@ -971,11 +985,12 @@ public function delete_fb_product( $product ) { /** * Updates Facebook Visibility upon trashing and restore. * + * @param string $new_status + * @param string $old_status + * @param \WP_post $post + * * @internal * - * @param string $new_status - * @param string $old_status - * @param \WP_post $post */ public function fb_change_product_published_status( $new_status, $old_status, $post ) { if ( ! $post ) { @@ -1017,9 +1032,10 @@ public function fb_change_product_published_status( $new_status, $old_status, $p /** * Re-publish restored variable product. * + * @param int $post_id + * * @internal * - * @param int $post_id */ public function fb_restore_untrashed_variable_product( $post_id ) { $product = wc_get_product( $post_id ); @@ -1048,11 +1064,12 @@ public function fb_restore_untrashed_variable_product( $post_id ) { * Change from trash status -> publish status * No need to update for change from trash <-> unpublish status * - * @since 2.0.2 - * * @param string $new_status * @param string $old_status + * * @return bool + * @since 2.0.2 + * */ private function should_update_visibility_for_product_status_change( $new_status, $old_status ) { return ( $old_status === 'publish' && $new_status !== 'publish' ) || ( $old_status === 'trash' && $new_status === 'publish' ) || ( $old_status === 'future' && $new_status === 'publish' ); @@ -1061,8 +1078,9 @@ private function should_update_visibility_for_product_status_change( $new_status /** * Deletes a product from Facebook when status is changed to draft. * - * @since 3.0.27 * @param \WP_post $post + * + * @since 3.0.27 */ public function delete_draft_product( $post ) { @@ -1070,7 +1088,7 @@ public function delete_draft_product( $post ) { return; } - $this->on_product_delete ( $post->ID ); + $this->on_product_delete( $post->ID ); } @@ -1102,7 +1120,7 @@ public function on_product_publish( $product_id ) { * If the user has opt-in to remove products that are out of stock, * this function will delete the product from FB Page as well. * - * @param int $wp_id + * @param int $wp_id * @param WC_Product $woo_product * * @return bool @@ -1111,15 +1129,17 @@ public function delete_on_out_of_stock( int $wp_id, WC_Product $woo_product ): b if ( Products::product_should_be_deleted( $woo_product ) ) { $product = wc_get_product( $wp_id ); $this->delete_fb_product( $product ); + return true; } + return false; } /** * Syncs product to Facebook when saving a variable product. * - * @param int $wp_id product post ID + * @param int $wp_id product post ID * @param WC_Facebook_Product|null $woo_product product object */ public function on_variable_product_publish( $wp_id, $woo_product = null ) { @@ -1162,9 +1182,10 @@ public function on_variable_product_publish( $wp_id, $woo_product = null ) { /** * Syncs product to Facebook when saving a simple product. * - * @param int $wp_id product post ID + * @param int $wp_id product post ID * @param WC_Facebook_Product|null $woo_product product object * @param WC_Facebook_Product|null $parent_product parent object + * * @return int|mixed|void|null */ public function on_simple_product_publish( $wp_id, $woo_product = null, &$parent_product = null ) { @@ -1187,6 +1208,7 @@ public function on_simple_product_publish( $wp_id, $woo_product = null, &$parent if ( $fb_product_item_id ) { $woo_product->fb_visibility = Products::is_product_visible( $woo_product->woo_product ); $this->update_product_item_batch_api( $woo_product, $fb_product_item_id ); + return $fb_product_item_id; } else { // Check if this is a new product item for an existing product group @@ -1215,13 +1237,15 @@ public function on_simple_product_publish( $wp_id, $woo_product = null, &$parent /** * Determines whether the product with the given ID should be synced. * + * @param WC_Product $product product object + * * @since 2.0.0 * - * @param WC_Product $product product object */ public function product_should_be_synced( WC_Product $product ): bool { try { $this->facebook_for_woocommerce->get_product_sync_validator( $product )->validate(); + return true; } catch ( \Exception $e ) { return false; @@ -1232,7 +1256,8 @@ public function product_should_be_synced( WC_Product $product ): bool { * Create product group and product, store fb-specific info. * * @param WC_Facebook_Product $woo_product - * @param string|null $fb_product_group_id + * @param string|null $fb_product_group_id + * * @return string */ public function create_product_simple( WC_Facebook_Product $woo_product, string $fb_product_group_id = null ): string { @@ -1245,13 +1270,15 @@ public function create_product_simple( WC_Facebook_Product $woo_product, string if ( $fb_product_group_id ) { return $this->create_product_item_batch_api( $woo_product, $retailer_id, $fb_product_group_id ); } + return ''; } /** * @param WC_Facebook_Product $woo_product - * @param string $retailer_id - * @param bool $variants + * @param string $retailer_id + * @param bool $variants + * * @return ?string */ public function create_product_group( WC_Facebook_Product $woo_product, string $retailer_id, bool $variants = false ): ?string { @@ -1276,12 +1303,14 @@ public function create_product_group( WC_Facebook_Product $woo_product, string $ self::FB_PRODUCT_GROUP_ID, $fb_product_group_id ); + return $fb_product_group_id; } } catch ( ApiException $e ) { $message = sprintf( 'There was an error trying to create the product group: %s', $e->getMessage() ); WC_Facebookcommerce_Utils::log( $message ); } + return null; } @@ -1306,7 +1335,7 @@ public function update_product_group( WC_Facebook_Product $woo_product ) { if ( ! $variants ) { WC_Facebookcommerce_Utils::log( sprintf( - /* translators: %1$s is referring to facebook product group id. */ + /* translators: %1$s is referring to facebook product group id. */ __( 'Nothing to update for product group for %1$s', 'facebook-for-woocommerce' @@ -1314,6 +1343,7 @@ public function update_product_group( WC_Facebook_Product $woo_product ) { $fb_product_group_id ) ); + return; } @@ -1353,10 +1383,10 @@ public function update_product_group( WC_Facebook_Product $woo_product ) { * Creates a product item using the facebook Catalog Batch API. This replaces existing functionality, * which is currently using facebook Product Item API implemented by `WC_Facebookcommerce_Integration::create_product_item` * - * @since 3.1.7 * @param WC_Facebook_Product $woo_product - * @param string $retailer_id - **/ + * @param string $retailer_id + **@since 3.1.7 + */ public function create_product_item_batch_api( $woo_product, $retailer_id, $product_group_id ): string { try { $product_data = $woo_product->prepare_product( $retailer_id, \WC_Facebook_Product::PRODUCT_PREP_TYPE_ITEMS_BATCH ); @@ -1375,6 +1405,7 @@ public function create_product_item_batch_api( $woo_product, $retailer_id, $prod $message = sprintf( 'There was an error trying to create a product item: %s', $e->getMessage() ); WC_Facebookcommerce_Utils::log( $message ); } + return ''; } @@ -1404,6 +1435,7 @@ public function create_product_item( $woo_product, $retailer_id, $product_group_ $message = sprintf( 'There was an error trying to create a product item: %s', $e->getMessage() ); WC_Facebookcommerce_Utils::log( $message ); } + return ''; } @@ -1411,25 +1443,26 @@ public function create_product_item( $woo_product, $retailer_id, $product_group_ * Determines if there is a matching variation for the default attributes. * Select closest matching if best can't be found. * - * @since 2.6.6 - * The algorithm only considers the variations that already have been synchronized to the catalog successfully. + * @param WC_Facebook_Product $woo_product + * @param string $fb_product_group_id * + * @return integer|null Facebook Catalog variation id. * @since 2.1.2 * - * @param WC_Facebook_Product $woo_product - * @param string $fb_product_group_id - * @return integer|null Facebook Catalog variation id. + * @since 2.6.6 + * The algorithm only considers the variations that already have been synchronized to the catalog successfully. + * */ private function get_product_group_default_variation( WC_Facebook_Product $woo_product, string $fb_product_group_id ) { $default_attributes = $woo_product->woo_product->get_default_attributes( 'edit' ); - $default_variation = null; + $default_variation = null; // Fetch variations that exist in the catalog. $existing_catalog_variations = $this->find_variation_product_item_ids( $fb_product_group_id ); $existing_catalog_variations_retailer_ids = array_keys( $existing_catalog_variations ); // All woocommerce variations for the product. - $product_variations = $woo_product->woo_product->get_available_variations(); + $product_variations = $woo_product->woo_product->get_available_variations(); if ( ! empty( $default_attributes ) ) { @@ -1443,13 +1476,13 @@ private function get_product_group_default_variation( WC_Facebook_Product $woo_p ); // Check if currently processed variation exist in the catalog. - if (!in_array($fb_retailer_id, $existing_catalog_variations_retailer_ids)) { + if ( ! in_array( $fb_retailer_id, $existing_catalog_variations_retailer_ids ) ) { continue; } - $variation_attributes = $this->get_product_variation_attributes($variation); - $variation_attributes_count = count($variation_attributes); - $matching_attributes_count = count(array_intersect_assoc($default_attributes, $variation_attributes)); + $variation_attributes = $this->get_product_variation_attributes( $variation ); + $variation_attributes_count = count( $variation_attributes ); + $matching_attributes_count = count( array_intersect_assoc( $default_attributes, $variation_attributes ) ); // Check how much current variation matches the selected default attributes. if ( $matching_attributes_count === $variation_attributes_count ) { @@ -1468,12 +1501,13 @@ private function get_product_group_default_variation( WC_Facebook_Product $woo_p * Filter product group default variation. * This can be used to customize the choice of a default variation (e.g. choose one with the lowest price). * - * @since 2.6.25 * @param integer|null Facebook Catalog variation id. * @param \WC_Facebook_Product WooCommerce product. * @param string product group ID. * @param array List of available WC_Product variations. * @param array List of Product Item IDs indexed by the variation's retailer ID. + * + * @since 2.6.25 */ return apply_filters( 'wc_facebook_product_group_default_variation', @@ -1488,10 +1522,11 @@ private function get_product_group_default_variation( WC_Facebook_Product $woo_p /** * Parses given product variation for it's attributes * - * @since 2.1.2 - * * @param array $variation + * * @return array + * @since 2.1.2 + * */ private function get_product_variation_attributes( array $variation ): array { $final_attributes = []; @@ -1508,7 +1543,8 @@ private function get_product_variation_attributes( array $variation ): array { * Update existing product using batch API. * * @param WC_Facebook_Product $woo_product - * @param string $fb_product_item_id + * @param string $fb_product_item_id + * * @return void */ public function update_product_item_batch_api( WC_Facebook_Product $woo_product, string $fb_product_item_id ): void { @@ -1539,7 +1575,8 @@ public function update_product_item_batch_api( WC_Facebook_Product $woo_product, * Update existing product. * * @param WC_Facebook_Product $woo_product - * @param string $fb_product_item_id + * @param string $fb_product_item_id + * * @return void */ public function update_product_item( WC_Facebook_Product $woo_product, string $fb_product_item_id ): void { @@ -1572,11 +1609,11 @@ public function update_product_item( WC_Facebook_Product $woo_product, string $f /** * Create or update product set * - * @since 2.3.0 - * * @param array $product_set_data Product Set data. - * @param int $product_set_id Product Set Term Id. - **/ + * @param int $product_set_id Product Set Term Id. + **@since 2.3.0 + * + */ public function create_or_update_product_set_item( $product_set_data, $product_set_id ) { // check if exists in FB $fb_product_set_id = get_term_meta( $product_set_id, self::FB_PRODUCT_SET_ID, true ); @@ -1606,6 +1643,7 @@ public function create_or_update_product_set_item( $product_set_data, $product_s * Delete product set * * @param string $fb_product_set_id Facebook Product Set ID. + * * @return void * @throws ApiException * @throws \WooCommerce\Facebook\API\Exceptions\Request_Limit_Reached @@ -1630,6 +1668,7 @@ public function delete_product_set_item( string $fb_product_set_id ) { * - should_sync: Don't display if the product is not supposed to be synced. * * @param WP_Post $post Wordpress Post + * * @return void */ public function display_batch_api_completed( $post ) { @@ -1645,10 +1684,10 @@ public function display_batch_api_completed( $post ) { try { facebook_for_woocommerce()->get_product_sync_validator( $fb_product->woo_product )->validate(); } catch ( \Exception $e ) { - $should_sync = false; + $should_sync = false; } - if( $should_sync ) { + if ( $should_sync ) { if ( $fb_product->woo_product->is_type( 'variable' ) ) { $fb_product_item_id = $this->get_product_fbid( self::FB_PRODUCT_GROUP_ID, $post->ID, $fb_product->woo_product ); } else { @@ -1658,9 +1697,9 @@ public function display_batch_api_completed( $post ) { if ( $fb_product_item_id ) { $this->display_success_message( - '' . + '' . 'View product on Meta catalog' ); } @@ -1735,10 +1774,11 @@ public function ajax_check_feed_upload_status_v2() { /** * Display custom success message (sugar). * - * @deprecated 2.1.0 - * * @param string $msg + * * @return void + * @deprecated 2.1.0 + * */ public function display_success_message( string $msg ): void { $msg = self::FB_ADMIN_MESSAGE_PREPEND . $msg; @@ -1753,6 +1793,7 @@ public function display_success_message( string $msg ): void { * Display custom info message (sugar). * * @param string $msg + * * @return void */ public function display_info_message( string $msg ): void { @@ -1769,6 +1810,7 @@ public function display_info_message( string $msg ): void { * Call remove_sticky_message or wait for time out. * * @param string $msg + * * @return void */ public function display_sticky_message( string $msg ): void { @@ -1805,6 +1847,7 @@ public function remove_resync_message() { * Logs and stores custom error message (sugar). * * @param string $msg + * * @return void */ public function display_error_message( string $msg ): void { @@ -1816,6 +1859,7 @@ public function display_error_message( string $msg ): void { * Displays out of sync message if products are edited using WooCommerce Advanced Bulk Edit. * * @param string $import_id + * * @return void */ public function ajax_woo_adv_bulk_edit_compat( string $import_id ): void { @@ -1835,12 +1879,16 @@ public function ajax_woo_adv_bulk_edit_compat( string $import_id ): void { * Display import message. * * @param string $import_id + * * @return void */ public function wp_all_import_compat( string $import_id ): void { $import = new PMXI_Import_Record(); $import->getById( $import_id ); - if ( ! $import->isEmpty() && in_array( $import->options['custom_type'], [ 'product', 'product_variation' ], true ) ) { + if ( ! $import->isEmpty() && in_array( $import->options['custom_type'], [ + 'product', + 'product_variation' + ], true ) ) { $this->display_out_of_sync_message( 'import' ); } } @@ -1849,6 +1897,7 @@ public function wp_all_import_compat( string $import_id ): void { * Displays out of sync message. * * @param string $action_name + * * @return void */ public function display_out_of_sync_message( string $action_name ): void { @@ -1866,7 +1915,8 @@ public function display_out_of_sync_message( string $action_name ): void { * id error, update existing ID. * * @param stdClass $error_data - * @param int $wpid + * @param int $wpid + * * @return null **/ public function get_existing_fbid( stdClass $error_data, int $wpid ) { @@ -1876,6 +1926,7 @@ public function get_existing_fbid( stdClass $error_data, int $wpid ) { self::FB_PRODUCT_GROUP_ID, (string) $error_data->product_group_id ); + return $error_data->product_group_id; } elseif ( isset( $error_data->product_item_id ) ) { update_post_meta( @@ -1883,6 +1934,7 @@ public function get_existing_fbid( stdClass $error_data, int $wpid ) { self::FB_PRODUCT_ITEM_ID, (string) $error_data->product_item_id ); + return $error_data->product_item_id; } else { return null; @@ -1927,7 +1979,7 @@ public function get_sample_product_feed() { $feed_item = [ 'title' => strip_tags( $product_data['name'] ), 'availability' => $woo_product->is_in_stock() ? 'in stock' : - 'out of stock', + 'out of stock', 'description' => strip_tags( $product_data['description'] ), 'id' => $product_data['retailer_id'], 'image_link' => $product_data['image_url'], @@ -1940,6 +1992,7 @@ public function get_sample_product_feed() { // https://codex.wordpress.org/Function_Reference/wp_reset_postdata wp_reset_postdata(); ob_end_clean(); + return json_encode( [ $items ] ); } @@ -1965,6 +2018,7 @@ public function reset_all_products() { 'Not resetting any FBIDs from products, must call reset from admin context.' ); + return false; } @@ -1974,7 +2028,7 @@ public function reset_all_products() { $post_ids = get_posts( [ 'post_type' => 'product', - 'posts_per_page' => -1, + 'posts_per_page' => - 1, 'fields' => 'ids', ] ); @@ -1985,7 +2039,7 @@ public function reset_all_products() { get_posts( [ 'post_type' => 'product_variation', - 'posts_per_page' => -1, + 'posts_per_page' => - 1, 'post_parent' => $post_id, 'fields' => 'ids', ] @@ -1997,6 +2051,7 @@ public function reset_all_products() { $this->delete_post_meta_loop( $post_ids ); WC_Facebookcommerce_Utils::log( 'Product FBIDs deleted' ); + return true; } @@ -2104,7 +2159,7 @@ private function sync_facebook_products() { } $message = sprintf( - /* translators: Placeholders %s - error message */ + /* translators: Placeholders %s - error message */ __( 'There was an error trying to sync the products to Facebook. %s', 'facebook-for-woocommerce' ), $error_message ); @@ -2116,10 +2171,10 @@ private function sync_facebook_products() { /** * Syncs Facebook products using the background processor. * - * @since 1.10.2 * @return bool * @throws ApiException Some comment. * @throws PluginException If product sync disabled. + * @since 1.10.2 */ private function sync_facebook_products_using_background_processor() { if ( ! $this->is_product_sync_enabled() ) { @@ -2153,7 +2208,7 @@ private function sync_facebook_products_using_background_processor() { } try { - $catalog = $this->facebook_for_woocommerce->get_api()->get_catalog($this->get_product_catalog_id()); + $catalog = $this->facebook_for_woocommerce->get_api()->get_catalog( $this->get_product_catalog_id() ); } catch ( ApiException $e ) { $message = sprintf( 'There was an error trying to delete a product set item: %s', $e->getMessage() ); WC_Facebookcommerce_Utils::log( $message ); @@ -2223,7 +2278,7 @@ private function sync_facebook_products_using_background_processor() { ); $this->on_product_publish( $post_id ); - $count++; + $count ++; } WC_Facebookcommerce_Utils::log( 'Synced ' . $count . ' products' ); $this->remove_sticky_message(); @@ -2255,9 +2310,9 @@ public function ajax_fb_toggle_visibility() { /** * Gets the product catalog ID. * + * @return string * @since 1.10.0 * - * @return string */ public function get_product_catalog_id() { if ( ! is_string( $this->product_catalog_id ) ) { @@ -2268,10 +2323,11 @@ public function get_product_catalog_id() { /** * Filters the Facebook product catalog ID. * - * @since 1.10.0 - * * @param string $product_catalog_id Facebook product catalog ID * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return apply_filters( 'wc_facebook_product_catalog_id', $this->product_catalog_id, $this ); } @@ -2279,9 +2335,9 @@ public function get_product_catalog_id() { /** * Gets the external merchant settings ID. * + * @return string * @since 1.10.0 * - * @return string */ public function get_external_merchant_settings_id() { if ( ! is_string( $this->external_merchant_settings_id ) ) { @@ -2292,10 +2348,11 @@ public function get_external_merchant_settings_id() { /** * Filters the Facebook external merchant settings ID. * - * @since 1.10.0 - * * @param string $external_merchant_settings_id Facebook external merchant settings ID * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (string) apply_filters( 'wc_facebook_external_merchant_settings_id', $this->external_merchant_settings_id, $this ); } @@ -2303,9 +2360,9 @@ public function get_external_merchant_settings_id() { /** * Gets the feed ID. * + * @return string * @since 1.10.0 * - * @return string */ public function get_feed_id() { if ( ! is_string( $this->feed_id ) ) { @@ -2316,10 +2373,11 @@ public function get_feed_id() { /** * Filters the Facebook feed ID. * - * @since 1.10.0 - * * @param string $feed_id Facebook feed ID * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (string) apply_filters( 'wc_facebook_feed_id', $this->feed_id, $this ); } @@ -2327,9 +2385,9 @@ public function get_feed_id() { /*** * Gets the Facebook Upload ID. * + * @return string * @since 1.11.0 * - * @return string */ public function get_upload_id() { if ( ! is_string( $this->upload_id ) ) { @@ -2340,10 +2398,11 @@ public function get_upload_id() { /** * Filters the Facebook upload ID. * - * @since 1.11.0 - * * @param string $upload_id Facebook upload ID * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.11.0 + * */ return (string) apply_filters( 'wc_facebook_upload_id', $this->upload_id, $this ); } @@ -2351,9 +2410,9 @@ public function get_upload_id() { /** * Gets the Facebook pixel install time in UTC seconds. * + * @return int * @since 1.10.0 * - * @return int */ public function get_pixel_install_time() { if ( ! (int) $this->pixel_install_time ) { @@ -2364,10 +2423,11 @@ public function get_pixel_install_time() { /** * Filters the Facebook pixel install time. * - * @since 1.10.0 - * * @param string $pixel_install_time Facebook pixel install time in UTC seconds, or null if none set * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (int) apply_filters( 'wc_facebook_pixel_install_time', $this->pixel_install_time, $this ); } @@ -2375,9 +2435,9 @@ public function get_pixel_install_time() { /** * Gets the configured JS SDK version. * + * @return string * @since 1.10.0 * - * @return string */ public function get_js_sdk_version() { if ( ! is_string( $this->js_sdk_version ) ) { @@ -2388,10 +2448,11 @@ public function get_js_sdk_version() { /** * Filters the Facebook JS SDK version. * - * @since 1.10.0 - * * @param string $js_sdk_version Facebook JS SDK version * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (string) apply_filters( 'wc_facebook_js_sdk_version', $this->js_sdk_version, $this ); } @@ -2399,18 +2460,19 @@ public function get_js_sdk_version() { /** * Gets the configured Facebook page ID. * + * @return string * @since 1.10.0 * - * @return string */ public function get_facebook_page_id() { /** * Filters the configured Facebook page ID. * - * @since 1.10.0 - * * @param string $page_id the configured Facebook page ID * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (string) apply_filters( 'wc_facebook_page_id', get_option( self::SETTING_FACEBOOK_PAGE_ID, '' ), $this ); } @@ -2418,18 +2480,19 @@ public function get_facebook_page_id() { /** * Gets the configured Facebook pixel ID. * + * @return string * @since 1.10.0 * - * @return string */ public function get_facebook_pixel_id() { /** * Filters the configured Facebook pixel ID. * - * @since 1.10.0 - * * @param string $pixel_id the configured Facebook pixel ID * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (string) apply_filters( 'wc_facebook_pixel_id', get_option( self::SETTING_FACEBOOK_PIXEL_ID, '' ), $this ); } @@ -2437,18 +2500,19 @@ public function get_facebook_pixel_id() { /** * Gets the IDs of the categories to be excluded from sync. * + * @return int[] * @since 1.10.0 * - * @return int[] */ public function get_excluded_product_category_ids() { /** * Filters the configured excluded product category IDs. * - * @since 1.10.0 - * * @param int[] $category_ids the configured excluded product category IDs * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (array) apply_filters( 'wc_facebook_excluded_product_category_ids', get_option( self::SETTING_EXCLUDED_PRODUCT_CATEGORY_IDS, [] ), $this ); } @@ -2456,18 +2520,19 @@ public function get_excluded_product_category_ids() { /** * Gets the IDs of the tags to be excluded from sync. * + * @return int[] * @since 1.10.0 * - * @return int[] */ public function get_excluded_product_tag_ids() { /** * Filters the configured excluded product tag IDs. * - * @since 1.10.0 - * * @param int[] $tag_ids the configured excluded product tag IDs * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (array) apply_filters( 'wc_facebook_excluded_product_tag_ids', get_option( self::SETTING_EXCLUDED_PRODUCT_TAG_IDS, [] ), $this ); } @@ -2475,18 +2540,19 @@ public function get_excluded_product_tag_ids() { /** * Gets the configured product description mode. * + * @return string * @since 1.10.0 * - * @return string */ public function get_product_description_mode() { /** * Filters the configured product description mode. * - * @since 1.10.0 - * * @param string $mode the configured product description mode * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ $mode = (string) apply_filters( 'wc_facebook_product_description_mode', get_option( self::SETTING_PRODUCT_DESCRIPTION_MODE, self::PRODUCT_DESCRIPTION_MODE_STANDARD ), $this ); @@ -2508,9 +2574,10 @@ public function get_product_description_mode() { /** * Updates the Facebook product catalog ID. * + * @param string $value product catalog ID value + * * @since 1.10.0 * - * @param string $value product catalog ID value */ public function update_product_catalog_id( $value ) { $this->product_catalog_id = $this->sanitize_facebook_credential( $value ); @@ -2521,9 +2588,10 @@ public function update_product_catalog_id( $value ) { /** * Updates the Facebook external merchant settings ID. * + * @param string $value external merchant settings ID value + * * @since 1.10.0 * - * @param string $value external merchant settings ID value */ public function update_external_merchant_settings_id( $value ) { $this->external_merchant_settings_id = $this->sanitize_facebook_credential( $value ); @@ -2534,9 +2602,10 @@ public function update_external_merchant_settings_id( $value ) { /** * Updates the Facebook feed ID. * + * @param string $value feed ID value + * * @since 1.10.0 * - * @param string $value feed ID value */ public function update_feed_id( $value ) { $this->feed_id = $this->sanitize_facebook_credential( $value ); @@ -2547,9 +2616,10 @@ public function update_feed_id( $value ) { /** * Updates the Facebook upload ID. * + * @param string $value upload ID value + * * @since 1.11.0 * - * @param string $value upload ID value */ public function update_upload_id( $value ) { $this->upload_id = $this->sanitize_facebook_credential( $value ); @@ -2560,9 +2630,10 @@ public function update_upload_id( $value ) { /** * Updates the Facebook pixel install time. * + * @param int $value pixel install time, in UTC seconds + * * @since 1.10.0 * - * @param int $value pixel install time, in UTC seconds */ public function update_pixel_install_time( $value ) { $value = (int) $value; @@ -2575,9 +2646,10 @@ public function update_pixel_install_time( $value ) { /** * Updates the Facebook JS SDK version. * + * @param string $value JS SDK version + * * @since 1.10.0 * - * @param string $value JS SDK version */ public function update_js_sdk_version( $value ) { $this->js_sdk_version = $this->sanitize_facebook_credential( $value ); @@ -2588,10 +2660,11 @@ public function update_js_sdk_version( $value ) { /** * Sanitizes a value that's a Facebook credential. * - * @since 1.10.0 - * * @param string $value value to sanitize + * * @return string + * @since 1.10.0 + * */ private function sanitize_facebook_credential( $value ) { return wc_clean( is_string( $value ) ? $value : '' ); @@ -2600,9 +2673,9 @@ private function sanitize_facebook_credential( $value ) { /** * Determines whether Facebook for WooCommerce is configured. * + * @return bool * @since 1.10.0 * - * @return bool */ public function is_configured() { return $this->get_facebook_page_id() && $this->facebook_for_woocommerce->get_connection_handler()->is_connected(); @@ -2611,18 +2684,19 @@ public function is_configured() { /** * Determines whether advanced matching is enabled. * + * @return bool * @since 1.10.0 * - * @return bool */ public function is_advanced_matching_enabled() { /** * Filters whether advanced matching is enabled. * - * @since 1.10.0 - * * @param bool $is_enabled whether advanced matching is enabled * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (bool) apply_filters( 'wc_facebook_is_advanced_matching_enabled', true, $this ); } @@ -2630,57 +2704,59 @@ public function is_advanced_matching_enabled() { /** * Determines whether product sync is enabled. * + * @return bool * @since 1.10.0 * - * @return bool */ public function is_product_sync_enabled() { /** * Filters whether product sync is enabled. * - * @since 1.10.0 - * * @param bool $is_enabled whether product sync is enabled * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.0 + * */ return (bool) apply_filters( 'wc_facebook_is_product_sync_enabled', 'yes' === get_option( self::SETTING_ENABLE_PRODUCT_SYNC, 'yes' ), $this ); } /** - * Return true if (legacy) feed generation is enabled. - * - * Feed generation for product sync is enabled by default, and generally recommended. - * Large stores, or stores running on shared hosting (low resources) may have issues - * with feed generation. This option allows those stores to disable generation to - * work around the issue. - * - * Note - this is temporary. In a future release, an improved feed system will be - * implemented, which should work well for all stores. This option will not disable - * the new improved implementation. - * - * @since 2.5.0 - * - * @return bool - */ - public function is_legacy_feed_file_generation_enabled() { - return 'yes' === get_option( self::OPTION_LEGACY_FEED_FILE_GENERATION_ENABLED, 'yes' ); - } + * Return true if (legacy) feed generation is enabled. + * + * Feed generation for product sync is enabled by default, and generally recommended. + * Large stores, or stores running on shared hosting (low resources) may have issues + * with feed generation. This option allows those stores to disable generation to + * work around the issue. + * + * Note - this is temporary. In a future release, an improved feed system will be + * implemented, which should work well for all stores. This option will not disable + * the new improved implementation. + * + * @return bool + * @since 2.5.0 + * + */ + public function is_legacy_feed_file_generation_enabled() { + return 'yes' === get_option( self::OPTION_LEGACY_FEED_FILE_GENERATION_ENABLED, 'yes' ); + } /** * Determines whether debug mode is enabled. * + * @return bool * @since 1.10.2 * - * @return bool */ public function is_debug_mode_enabled() { /** * Filters whether debug mode is enabled. * - * @since 1.10.2 - * * @param bool $is_enabled whether debug mode is enabled * @param \WC_Facebookcommerce_Integration $integration the integration instance + * + * @since 1.10.2 + * */ return (bool) apply_filters( 'wc_facebook_is_debug_mode_enabled', 'yes' === get_option( self::SETTING_ENABLE_DEBUG_MODE ), $this ); } @@ -2688,9 +2764,9 @@ public function is_debug_mode_enabled() { /** * Determines whether debug mode is enabled. * + * @return bool * @since 2.6.6 * - * @return bool */ public function is_new_style_feed_generation_enabled() { return (bool) ( 'yes' === get_option( self::SETTING_ENABLE_NEW_STYLE_FEED_GENERATOR ) ); @@ -2710,15 +2786,16 @@ public function are_headers_requested_for_debug() { /*** * Determines if the feed has been migrated from FBE 1 to FBE 1.5 * + * @return bool * @since 1.11.0 * - * @return bool */ public function is_feed_migrated() { if ( ! is_bool( $this->feed_migrated ) ) { $value = get_option( 'wc_facebook_feed_migrated', 'no' ); $this->feed_migrated = wc_string_to_bool( $value ); } + return $this->feed_migrated; } @@ -2727,6 +2804,7 @@ public function is_feed_migrated() { * * @param string $message * @param string $type + * * @return string */ private function get_message_html( string $message, string $type = 'error' ): string { @@ -2804,6 +2882,7 @@ public function admin_options() { * Delete product item by id. * * @param int $wp_id + * * @return void */ public function delete_product_item( int $wp_id ): void { @@ -2825,9 +2904,10 @@ public function delete_product_item( int $wp_id ): void { /** * Uses the Graph API to delete the Product Group associated with the given product. * + * @param int $product_id product ID + * * @since 2.0.0 * - * @param int $product_id product ID */ public function delete_product_group( int $product_id ) { $product_group_id = $this->get_product_fbid( self::FB_PRODUCT_GROUP_ID, $product_id ); @@ -2847,11 +2927,13 @@ public function delete_product_group( int $product_id ) { * Filter function for woocommerce_duplicate_product_exclude_meta filter. * * @param array $to_delete + * * @return array */ public function fb_duplicate_product_reset_meta( array $to_delete ): array { $to_delete[] = self::FB_PRODUCT_ITEM_ID; $to_delete[] = self::FB_PRODUCT_GROUP_ID; + return $to_delete; } @@ -2859,7 +2941,7 @@ public function fb_duplicate_product_reset_meta( array $to_delete ): array { * Helper function to update FB visibility. * * @param int|WC_Product $product_id product ID or product object - * @param string $visibility visibility + * @param string $visibility visibility */ public function update_fb_visibility( $product_id, $visibility ) { // bail if the plugin is not configured properly @@ -2900,7 +2982,8 @@ public function update_fb_visibility( $product_id, $visibility ) { $fb_product_item_id = $this->get_product_fbid( self::FB_PRODUCT_ITEM_ID, $product->get_id() ); if ( ! $fb_product_item_id ) { \WC_Facebookcommerce_Utils::fblog( $fb_product_item_id . " doesn't exist but underwent a visibility transform.", [], true ); - return; + + return; } try { $set_visibility = $this->facebook_for_woocommerce->get_api()->update_product_item( $fb_product_item_id, [ 'visibility' => $visibility ] ); @@ -2917,9 +3000,10 @@ public function update_fb_visibility( $product_id, $visibility ) { /** * Sync product upon quick or bulk edit save action. * + * @param \WC_Product $product product object + * * @internal * - * @param \WC_Product $product product object */ public function on_quick_and_bulk_edit_save( $product ) { // bail if not a product or product is not enabled for sync @@ -2943,9 +3027,10 @@ public function on_quick_and_bulk_edit_save( $product ) { /** * Gets Facebook product ID from meta or from Facebook API. * - * @param string $fbid_type ID type (group or item) - * @param int $wp_id post ID + * @param string $fbid_type ID type (group or item) + * @param int $wp_id post ID * @param WC_Facebook_Product|null $woo_product product + * * @return string facebook product id or an empty string */ public function get_product_fbid( string $fbid_type, int $wp_id, $woo_product = null ) { @@ -2981,7 +3066,7 @@ public function get_product_fbid( string $fbid_type, int $wp_id, $woo_product = WC_Facebookcommerce_Utils::log( $e->getMessage() ); $this->display_error_message( sprintf( - /* translators: Placeholders %1$s - original error message from Facebook API */ + /* translators: Placeholders %1$s - original error message from Facebook API */ esc_html__( 'There was an issue connecting to the Facebook API: %s', 'facebook-for-woocommerce' ), $e->getMessage() ) @@ -3007,9 +3092,9 @@ public function ajax_display_test_result() { $response['pass'] = 'false'; $response['debug_info'] = get_transient( 'facebook_plugin_test_fail' ); $response['stack_trace'] = - get_transient( 'facebook_plugin_test_stack_trace' ); + get_transient( 'facebook_plugin_test_stack_trace' ); $response['stack_trace'] = - preg_replace( "/\n/", '
', $response['stack_trace'] ); + preg_replace( "/\n/", '
', $response['stack_trace'] ); delete_transient( 'facebook_plugin_test_fail' ); delete_transient( 'facebook_plugin_test_stack_trace' ); } @@ -3018,4 +3103,15 @@ public function ajax_display_test_result() { wp_die(); } + /** + * Determines if the enhanced onboarding (iframe) should be used. + * + * @return bool + * @since 2.0.0 + * + */ + public function use_enhanced_onboarding() { + return false; + } + } diff --git a/includes/Admin/Settings_Screens/Connection.php b/includes/Admin/Settings_Screens/Connection.php index 14ad3ada1..cc6daa5ba 100644 --- a/includes/Admin/Settings_Screens/Connection.php +++ b/includes/Admin/Settings_Screens/Connection.php @@ -1,5 +1,4 @@ is_current_screen_page() ) { + wp_enqueue_script( 'wp-api' ); + } } /** @@ -46,6 +60,14 @@ public function initHook(): void { $this->title = __( 'Connection', 'facebook-for-woocommerce' ); } + /** + * Determines if we should use enhanced onboarding. + * + * @return bool + */ + protected function use_enhanced_onboarding() { + return facebook_for_woocommerce()->get_integration()->use_enhanced_onboarding(); + } /** * Adds admin notices. @@ -60,7 +82,7 @@ public function add_notices() { if ( get_transient( 'wc_facebook_connection_failed' ) ) { $message = sprintf( - /* translators: Placeholders: %1$s - tag, %2$s - tag, %3$s - tag, %4$s - tag, %5$s - tag, %6$s - tag */ + /* translators: Placeholders: %1$s - tag, %2$s - tag, %3$s - tag, %4$s - tag, %5$s - tag, %6$s - tag */ __( '%1$sHeads up!%2$s It looks like there was a problem with reconnecting your site to Facebook. Please %3$sclick here%4$s to try again, or %5$sget in touch with our support team%6$s for assistance.', 'facebook-for-woocommerce' ), '', '', @@ -87,8 +109,6 @@ public function add_notices() { * Enqueue the assets. * * @internal - * - * @since 2.0.0 */ public function enqueue_assets() { @@ -106,6 +126,12 @@ public function enqueue_assets() { * @since 2.0.0 */ public function render() { + // Check if we should render iframe + if ( $this->use_enhanced_onboarding() ) { + $this->render_facebook_iframe(); + + return; + } $is_connected = facebook_for_woocommerce()->get_connection_handler()->is_connected(); @@ -166,11 +192,12 @@ public function render() { // if the catalog ID is set, update the URL and try to get its name for display $catalog_id = $static_items['catalog']['value']; - if ( !empty( $catalog_id ) ) { + if ( ! empty( $catalog_id ) ) { $static_items['catalog']['url'] = "https://www.facebook.com/commerce/catalogs/{$catalog_id}/products/"; try { $response = facebook_for_woocommerce()->get_api()->get_catalog( $catalog_id ); - if ( $name = $response->name ) { + $name = $response->name ?? ''; + if ( $name ) { $static_items['catalog']['value'] = $name; } } catch ( ApiException $exception ) { @@ -190,59 +217,58 @@ public function render() { - $item ) : + $item ) : - $item = wp_parse_args( - $item, - array( - 'label' => '', - 'value' => '', - 'url' => '', - ) - ); + $item = wp_parse_args( + $item, + array( + 'label' => '', + 'value' => '', + 'url' => '', + ) + ); - ?> + ?> - + - + - - + + - +
@@ -252,16 +278,51 @@ class="dashicons dashicons-external" parent::render(); } + /** + * Renders the appropriate Facebook iframe based on connection status. + */ + private function render_facebook_iframe() { + $connection = facebook_for_woocommerce()->get_connection_handler(); + $is_connected = $connection->is_connected(); + $merchant_access_token = get_option( 'wc_facebook_merchant_access_token', '' ); + + if ( ! empty( $merchant_access_token ) && $is_connected ) { + // Get management iframe URL for connected merchants + $iframe_url = \WooCommerce\Facebook\Handlers\MetaExtension::generate_iframe_management_url( + $connection->get_external_business_id() + ); + } else { + // Get onboarding iframe URL for new connections + $iframe_url = \WooCommerce\Facebook\Handlers\MetaExtension::generate_iframe_splash_url( + $is_connected, + $connection->get_plugin(), + $connection->get_external_business_id() + ); + } + + if ( empty( $iframe_url ) ) { + return; + } + + ?> + + -
- -

- -
- - - + - - - + - -
-
+ is_current_screen_page() ) { + return; + } + + // Check if we have a merchant access token + $merchant_access_token = get_option( 'wc_facebook_merchant_access_token', '' ); + + if ( ! $this->use_enhanced_onboarding() ) { + return; + } + ?> + __( 'Enable debug mode', 'facebook-for-woocommerce' ), 'type' => 'checkbox', 'desc' => __( 'Log plugin events for debugging.', 'facebook-for-woocommerce' ), - /** - * Translators: %s URL to the documentation page. - */ + /* translators: %s URL to the documentation page. */ 'desc_tip' => sprintf( __( 'Only enable this if you are experiencing problems with the plugin. Learn more.', 'facebook-for-woocommerce' ), 'https://woocommerce.com/document/facebook-for-woocommerce/#debug-tools' ), 'default' => 'no', ), @@ -352,17 +491,11 @@ public function get_settings() { 'title' => __( 'Experimental! Enable new style feed generation', 'facebook-for-woocommerce' ), 'type' => 'checkbox', 'desc' => __( 'Use new, memory improved, feed generation process.', 'facebook-for-woocommerce' ), - /** - * Translators: %s URL to the documentation page. - */ + /* translators: %s URL to the documentation page. */ 'desc_tip' => sprintf( __( 'This is an experimental feature in testing phase. Only enable this if you are experiencing problems with feed generation. Learn more.', 'facebook-for-woocommerce' ), 'https://woocommerce.com/document/facebook-for-woocommerce/#feed-generation' ), 'default' => 'no', ), - array( 'type' => 'sectionend' ), - ); } - - } diff --git a/includes/Handlers/Connection.php b/includes/Handlers/Connection.php index d7acdc36b..39a4e2114 100644 --- a/includes/Handlers/Connection.php +++ b/includes/Handlers/Connection.php @@ -951,11 +951,11 @@ private function get_connect_parameters_extras() { /** * Gets the configured timezone string using values accepted by Facebook * - * @since 2.0.0 + * @since 2.5.0 * * @return string */ - private function get_timezone_string() { + public function get_timezone_string() { $timezone = wc_timezone_string(); // convert +05:30 and +05:00 into Etc/GMT+5 - we ignore the minutes because Facebook does not allow minute offsets if ( preg_match( '/([+-])(\d{2}):\d{2}/', $timezone, $matches ) ) { diff --git a/includes/Handlers/MetaExtension.php b/includes/Handlers/MetaExtension.php new file mode 100644 index 000000000..91a56c464 --- /dev/null +++ b/includes/Handlers/MetaExtension.php @@ -0,0 +1,513 @@ + 400 ) + ); + } + + return true; + } + + /** + * Updates Facebook settings options. + * + * @param array $settings Array of settings to update. + * + * @return void + * @since 2.0.0 + */ + private static function update_settings( $settings ) { + foreach ( $settings as $key => $value ) { + if ( ! empty( $key ) ) { + update_option( $key, $value ); + } + } + } + + /** + * Sanitizes and retrieves a value from an array. + * + * @param array $data Array to retrieve value from. + * @param string $key Key to retrieve. + * @param bool $sanitize Whether to sanitize the value. + * + * @return mixed|string The value or empty string if not set. + * @since 2.0.0 + */ + private static function get_param_value( $data, $key, $sanitize = true ) { + if ( ! isset( $data[ $key ] ) ) { + return ''; + } + + $value = $data[ $key ]; + + if ( $sanitize && is_string( $value ) ) { + return sanitize_text_field( $value ); + } + + return $value; + } + + /** + * Maps request parameters to option names. + * + * @param array $params Request parameters. + * + * @return array Mapped options with values. + * @since 2.0.0 + */ + private static function map_params_to_options( $params ) { + $options = array(); + + // Define parameter to option mapping + $mapping = array( + 'access_token' => self::OPTION_ACCESS_TOKEN, + 'merchant_access_token' => self::OPTION_MERCHANT_ACCESS_TOKEN, + 'page_access_token' => self::OPTION_PAGE_ACCESS_TOKEN, + 'system_user_id' => self::OPTION_SYSTEM_USER_ID, + 'business_manager_id' => self::OPTION_BUSINESS_MANAGER_ID, + 'ad_account_id' => self::OPTION_AD_ACCOUNT_ID, + 'instagram_business_id' => self::OPTION_INSTAGRAM_BUSINESS_ID, + 'commerce_merchant_settings_id' => self::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, + 'external_business_id' => self::OPTION_EXTERNAL_BUSINESS_ID, + 'commerce_partner_integration_id' => self::OPTION_COMMERCE_PARTNER_INTEGRATION_ID, + 'product_catalog_id' => self::OPTION_PRODUCT_CATALOG_ID, + 'pixel_id' => self::OPTION_PIXEL_ID, + 'profiles' => self::OPTION_PROFILES, + 'installed_features' => self::OPTION_INSTALLED_FEATURES, + // Integration settings with special handling + 'page_id' => \WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PAGE_ID, + 'catalog_id' => \WC_Facebookcommerce_Integration::OPTION_PRODUCT_CATALOG_ID, + ); + + // Process each parameter + foreach ( $mapping as $param_key => $option_name ) { + if ( isset( $params[ $param_key ] ) ) { + // Skip if this is an alias and we've already processed the canonical field + if ( 'product_catalog_id' === $param_key && isset( $params['catalog_id'] ) ) { + continue; + } + + // Determine if we should sanitize + $sanitize = ! in_array( $param_key, array( 'profiles', 'installed_features' ), true ); + + $options[ $option_name ] = self::get_param_value( $params, $param_key, $sanitize ); + } + } + + return $options; + } + + /** + * Updates connection status flags based on tokens. + * + * @param array $params Parameters containing tokens. + * + * @return void + * @since 2.0.0 + */ + private static function update_connection_status( $params ) { + if ( ! empty( $params['access_token'] ) ) { + update_option( self::OPTION_HAS_CONNECTED_FBE_2, 'yes' ); + } + + if ( ! empty( $params['page_access_token'] ) ) { + update_option( self::OPTION_HAS_AUTHORIZED_PAGES, 'yes' ); + } + } + + /** + * Clears Facebook integration options. + * + * @return void + * @since 2.0.0 + */ + private static function clear_integration_options() { + $options = array( + // Connection handler options + self::OPTION_ACCESS_TOKEN, + self::OPTION_MERCHANT_ACCESS_TOKEN, + self::OPTION_PAGE_ACCESS_TOKEN, + self::OPTION_SYSTEM_USER_ID, + self::OPTION_BUSINESS_MANAGER_ID, + self::OPTION_AD_ACCOUNT_ID, + self::OPTION_INSTAGRAM_BUSINESS_ID, + self::OPTION_COMMERCE_MERCHANT_SETTINGS_ID, + self::OPTION_EXTERNAL_BUSINESS_ID, + self::OPTION_COMMERCE_PARTNER_INTEGRATION_ID, + + // Additional data stored during connection + self::OPTION_PROFILES, + self::OPTION_INSTALLED_FEATURES, + + // Connection status flags + self::OPTION_HAS_CONNECTED_FBE_2, + self::OPTION_HAS_AUTHORIZED_PAGES, + ); + + // Clear all options + foreach ( $options as $option_name ) { + if ( in_array( $option_name, array( self::OPTION_PROFILES, self::OPTION_INSTALLED_FEATURES ), true ) ) { + update_option( $option_name, null ); + } elseif ( in_array( + $option_name, + array( + self::OPTION_HAS_CONNECTED_FBE_2, + self::OPTION_HAS_AUTHORIZED_PAGES, + ), + true + ) ) { + update_option( $option_name, 'no' ); + } else { + update_option( $option_name, '' ); + } + } + + // Integration settings - use constants for consistency + update_option( \WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PAGE_ID, '' ); + update_option( \WC_Facebookcommerce_Integration::SETTING_FACEBOOK_PIXEL_ID, '' ); + } + + // ========================== + // = API Communication = + // ========================== + + /** + * Makes an API call to Facebook's Graph API. + * + * @param string $method HTTP method (GET, POST, etc.) + * @param string $endpoint API endpoint + * @param array $params Request parameters + * + * @return array Response data + * @throws \Exception If the request fails. + */ + private static function call_api( $method, $endpoint, $params ) { + $url = 'https://graph.facebook.com/' . self::API_VERSION . '/' . $endpoint; + + if ( 'GET' === $method ) { + $url = add_query_arg( $params, $url ); + } + + $args = array( + 'method' => $method, + 'timeout' => 30, + 'headers' => array( + 'Content-Type' => 'application/json', + ), + ); + + if ( 'POST' === $method ) { + $args['body'] = wp_json_encode( $params ); + } + + $response = wp_remote_request( $url, $args ); + + if ( is_wp_error( $response ) ) { + throw new \Exception( $response->get_error_message() ); + } + + $status_code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body, true ); + + // Check for API errors + if ( $status_code >= 400 ) { + $error_message = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Unknown API error', 'facebook-for-woocommerce' ); + throw new \Exception( sprintf( 'Facebook API error (%d): %s', $status_code, $error_message ) ); + } + + return $data; + } + + // ========================== + // = REST API Endpoints = + // ========================== + + /** + * Initialize the REST API endpoint for updating Facebook settings. + * + * @return void + */ + public static function init_rest_endpoint() { + register_rest_route( + 'wc-facebook/v1', + 'update_fb_settings', + array( + 'methods' => 'POST', + 'callback' => array( __CLASS__, 'rest_update_fb_settings' ), + 'permission_callback' => array( __CLASS__, 'rest_update_fb_settings_permission_callback' ), + ) + ); + + register_rest_route( + 'wc-facebook/v1', + 'uninstall', + array( + 'methods' => 'POST', + 'callback' => array( __CLASS__, 'rest_handle_uninstall' ), + 'permission_callback' => array( __CLASS__, 'rest_update_fb_settings_permission_callback' ), + ) + ); + } + + /** + * Permission callback for the REST API endpoint. + * + * @return bool + */ + public static function rest_update_fb_settings_permission_callback() { + return current_user_can( 'manage_woocommerce' ); + } + + /** + * REST API endpoint callback to update Facebook settings. + * + * Expects POST parameters: + * - merchant_access_token: merchant access token (required). + * - access_token: system user access token (required). + * - page_access_token: page access token. + * - product_catalog_id: product catalog ID. + * - pixel_id: pixel ID. + * - page_id: page ID. + * - commerce_partner_integration_id: commerce partner integration ID. + * - profiles: profiles data. + * - installed_features: installed features data. + * + * @param WP_REST_Request $request The request. + * @return WP_REST_Response + */ + public static function rest_update_fb_settings( WP_REST_Request $request ) { + // Get JSON data from request body + $params = $request->get_json_params(); + + // Validate required tokens + $validation_result = self::validate_required_tokens( $params ); + if ( is_wp_error( $validation_result ) ) { + return rest_ensure_response( + array( + 'success' => false, + 'message' => $validation_result->get_error_message(), + ) + ); + } + + // Map parameters to options and update settings + $options = self::map_params_to_options( $params ); + self::update_settings( $options ); + + // Update connection status flags + self::update_connection_status( $params ); + + return new WP_REST_Response( + array( + 'success' => true, + 'message' => __( 'Facebook settings updated successfully', 'facebook-for-woocommerce' ), + ), + 200 + ); + } + + /** + * REST API endpoint callback to handle uninstall requests. + * + * @return WP_REST_Response|WP_Error + */ + public static function rest_handle_uninstall() { + try { + // Try to disconnect from Facebook API first + $external_business_id = get_option( self::OPTION_EXTERNAL_BUSINESS_ID, '' ); + if ( ! empty( $external_business_id ) ) { + try { + facebook_for_woocommerce()->get_api()->delete_mbe_connection( (string) $external_business_id ); + } catch ( \Exception $e ) { + facebook_for_woocommerce()->log( sprintf( 'Error during API uninstall: %s', $e->getMessage() ) ); + // Continue with local disconnection even if API call fails + } + } + + // Clear all integration options + self::clear_integration_options(); + + // Get the integration instance and update the product catalog ID + $integration = facebook_for_woocommerce()->get_integration(); + if ( $integration ) { + $integration->update_product_catalog_id( '' ); + } + + return new WP_REST_Response( + array( + 'success' => true, + 'message' => __( 'Facebook integration successfully uninstalled', 'facebook-for-woocommerce' ), + ), + 200 + ); + } catch ( \Exception $e ) { + return new WP_Error( + 'uninstall_error', + $e->getMessage(), + array( 'status' => 500 ) + ); + } + } + + // ========================== + // = IFrame Management = + // ========================== + + /** + * Generates the Commerce Hub iframe splash page URL. + * + * @param bool $is_connected Whether the plugin is currently connected. + * @param object $plugin The plugin instance. + * @param string $external_business_id External business ID. + * + * @return string + */ + public static function generate_iframe_splash_url( $is_connected, $plugin, $external_business_id ): string { + $connection_handler = facebook_for_woocommerce()->get_connection_handler(); + $external_client_metadata = array( + 'shop_domain' => wc_get_page_permalink( 'shop' ), + 'admin_url' => admin_url(), + 'client_version' => $plugin->get_version(), + 'commerce_partner_seller_platform_type' => 'SELF_SERVE_PLATFORM', + 'country_code' => WC()->countries->get_base_country(), + 'platform_store_id' => get_current_blog_id(), + ); + + return add_query_arg( + array( + 'access_client_token' => self::CLIENT_TOKEN, + 'business_vertical' => 'ECOMMERCE', + 'channel' => 'COMMERCE', + 'app_id' => facebook_for_woocommerce()->get_connection_handler()->get_client_id(), + 'business_name' => rawurlencode( $connection_handler->get_business_name() ), + 'currency' => get_woocommerce_currency(), + 'timezone' => $connection_handler->get_timezone_string(), + 'external_business_id' => $external_business_id, + 'installed' => $is_connected, + 'external_client_metadata' => rawurlencode( wp_json_encode( $external_client_metadata ) ), + ), + self::COMMERCE_HUB_URL . 'commerce_extension/splash/' + ); + } + + /** + * Generates the Commerce Hub iframe management page URL. + * + * @param string $external_business_id External business ID. + * + * @return string + */ + public static function generate_iframe_management_url( $external_business_id ) { + $access_token = get_option( self::OPTION_ACCESS_TOKEN, '' ); + + if ( empty( $access_token ) ) { + return ''; + } + + try { + $request = array( + 'access_token' => $access_token, + 'fields' => 'commerce_extension', + 'fbe_external_business_id' => $external_business_id, + ); + + $response = self::call_api( 'GET', 'fbe_business', $request ); + + if ( ! empty( $response['commerce_extension']['uri'] ) ) { + return $response['commerce_extension']['uri']; + } + } catch ( \Exception $e ) { + facebook_for_woocommerce()->log( 'Facebook Commerce Extension URL Error: ' . $e->getMessage() ); + } + + return ''; + } +} diff --git a/tests/Unit/Admin/Settings_Screens/ConnectionTest.php b/tests/Unit/Admin/Settings_Screens/ConnectionTest.php new file mode 100644 index 000000000..5dd38a6ff --- /dev/null +++ b/tests/Unit/Admin/Settings_Screens/ConnectionTest.php @@ -0,0 +1,173 @@ +connection = new Connection(); + } + + /** + * Helper method to invoke private/protected methods + * + * @param object $object Object instance + * @param string $methodName Method name to call + * @param array $parameters Parameters to pass into method + * + * @return mixed Method return value + */ + private function invoke_method($object, $methodName, array $parameters = []) { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } + + /** + * Test that iframe connection can be enabled or disabled + */ + public function test_use_iframe_connection() { + // Test the actual implementation (currently returns false) + $reflection = new \ReflectionClass($this->connection); + $method = $reflection->getMethod('use_enhanced_onboarding'); + $method->setAccessible(true); + + // REMOVE WHEN READY TO RELEASE ENHANCED ONBOARDING FLOW + // Test to ensure that the use_enhanced_onboarding method returns false + $actual_value = $method->invoke($this->connection); + $this->assertFalse($actual_value); + + // Create a mock that returns true for testing the true case + $connection_mock = $this->getMockBuilder(Connection::class) + ->onlyMethods(['use_enhanced_onboarding']) + ->getMock(); + + $connection_mock->method('use_enhanced_onboarding') + ->willReturn(true); + + // Use reflection to call the protected method on the mock + $reflection = new \ReflectionClass($connection_mock); + $method = $reflection->getMethod('use_enhanced_onboarding'); + $method->setAccessible(true); + $mock_value = $method->invoke($connection_mock); + + $this->assertTrue($mock_value); + } + + /** + * Test that render method calls render_facebook_iframe when enhanced onboarding is enabled + */ + public function test_render_facebook_box_iframe() { + // Create a partial mock of the Connection class + $connection = $this->getMockBuilder(Connection::class) + ->onlyMethods(['use_enhanced_onboarding']) + ->getMock(); + + // Configure the mock to return true for use_enhanced_onboarding + $connection->expects($this->once()) + ->method('use_enhanced_onboarding') + ->willReturn(true); + + // Start output buffering to capture the render output + ob_start(); + $connection->render(); + $output = ob_get_clean(); + + // Since we can't directly test the private render_facebook_iframe method, + // we'll verify that the render method doesn't output the legacy Facebook box + // when enhanced onboarding is enabled + $this->assertStringNotContainsString('wc-facebook-connection-box', $output); + } + + /** + * Test that render_message_handler outputs the expected JavaScript + */ + public function test_render_message_handler() { + // Create a mock of the Connection class + $connection_mock = $this->getMockBuilder(Connection::class) + ->onlyMethods(['is_current_screen_page', 'use_enhanced_onboarding']) + ->getMock(); + + // Configure the mock to return true for is_current_screen_page + $connection_mock->method('is_current_screen_page') + ->willReturn(true); + + // Configure the mock to return true for use_enhanced_onboarding + $connection_mock->method('use_enhanced_onboarding') + ->willReturn(true); + + ob_start(); + + // Call the method + $connection_mock->render_message_handler(); + + $output = ob_get_clean(); + + // Assert JavaScript event listeners and handlers + $this->assertStringContainsString('window.addEventListener(\'message\'', $output); + $this->assertStringContainsString('CommerceExtension::INSTALL', $output); + $this->assertStringContainsString('CommerceExtension::RESIZE', $output); + $this->assertStringContainsString('CommerceExtension::UNINSTALL', $output); + + // Assert fetch request setup - check for wpApiSettings.root instead of hardcoded path + $this->assertStringContainsString('fetch(wpApiSettings.root + \'wc-facebook/v1/update_fb_settings\'', $output); + $this->assertStringContainsString('method: \'POST\'', $output); + $this->assertStringContainsString('credentials: \'same-origin\'', $output); + } + + /** + * Test that render_message_handler doesn't output when not on current screen + */ + public function test_render_message_handler_not_current_screen() { + $connection_mock = $this->getMockBuilder(Connection::class) + ->onlyMethods(['is_current_screen_page']) + ->getMock(); + + $connection_mock->method('is_current_screen_page') + ->willReturn(false); + + ob_start(); + $connection_mock->render_message_handler(); + $output = ob_get_clean(); + + $this->assertEmpty($output); + } + + /** + * Test that the management URL is used when merchant token exists + */ + public function test_renders_management_url_based_on_merchant_token() { + // Create a mock for testing the private render_facebook_iframe method + $connection = $this->getMockBuilder(Connection::class) + ->onlyMethods(['use_enhanced_onboarding']) + ->getMock(); + + $connection->expects($this->any()) + ->method('use_enhanced_onboarding') + ->willReturn(true); + + // Set up the merchant token + update_option('wc_facebook_merchant_access_token', 'test_token'); + + // Use output buffering to capture the iframe HTML + ob_start(); + $this->invoke_method($connection, 'render_facebook_iframe'); + $output = ob_get_clean(); + + // Check that the iframe is rendered + $this->assertStringContainsString('assertStringContainsString('frameborder="0"', $output); + } +} \ No newline at end of file diff --git a/tests/Unit/Handlers/MetaExtensionTest.php b/tests/Unit/Handlers/MetaExtensionTest.php new file mode 100644 index 000000000..3b461affa --- /dev/null +++ b/tests/Unit/Handlers/MetaExtensionTest.php @@ -0,0 +1,204 @@ +meta_extension = new MetaExtension(); + } + + /** + * Test generate_iframe_splash_url + */ + public function test_generate_iframe_splash_url() { + $plugin = facebook_for_woocommerce(); + + $url = MetaExtension::generate_iframe_splash_url(true, $plugin, 'test_business_id'); + $handler = facebook_for_woocommerce()->get_connection_handler(); + + // Assert URL contains expected parameters + $this->assertStringContainsString('access_client_token=' . MetaExtension::CLIENT_TOKEN, $url); + $this->assertStringContainsString('app_id=', $url); + $this->assertStringContainsString('business_name=' . rawurlencode($handler->get_business_name()), $url); + $this->assertStringContainsString('external_business_id=test_business_id', $url); + $this->assertStringContainsString('installed=1', $url); + $this->assertStringContainsString('external_client_metadata=', $url); + $this->assertStringContainsString('https://www.commercepartnerhub.com/commerce_extension/splash/', $url); + } + + /** + * Test REST API token update with valid data + */ + public function test_rest_update_fb_settings_valid_data() { + // Create a mock for WP_REST_Request + $request = $this->getMockBuilder(WP_REST_Request::class) + ->disableOriginalConstructor() + ->setMethods(array('get_json_params')) + ->getMock(); + + // Set up the mock to return our test data + $request->expects($this->once()) + ->method('get_json_params') + ->willReturn([ + 'merchant_access_token' => 'test_merchant_token', + 'access_token' => 'test_access_token', + 'page_access_token' => 'test_page_token', + 'product_catalog_id' => '123456', + 'pixel_id' => '789012' + ]); + + $response = MetaExtension::rest_update_fb_settings($request); + + $this->assertInstanceOf(WP_REST_Response::class, $response); + $this->assertEquals(200, $response->get_status()); + $this->assertTrue($response->get_data()['success']); + + // Verify options were updated + $this->assertEquals('test_access_token', get_option('wc_facebook_access_token')); + $this->assertEquals('test_merchant_token', get_option('wc_facebook_merchant_access_token')); + $this->assertEquals('test_page_token', get_option('wc_facebook_page_access_token')); + $this->assertEquals('123456', get_option('wc_facebook_product_catalog_id')); + $this->assertEquals('789012', get_option('wc_facebook_pixel_id')); + } + + /** + * Test rest_update_fb_settings with missing merchant token + */ + public function test_rest_update_fb_settings_missing_merchant_token() { + // Create a mock of WP_REST_Request + $request = $this->getMockBuilder(WP_REST_Request::class) + ->disableOriginalConstructor() + ->onlyMethods(['get_json_params']) + ->getMock(); + + // Set up the mock to return our test data + $request->expects($this->once()) + ->method('get_json_params') + ->willReturn([ + 'access_token' => 'test_access_token', + 'page_access_token' => 'test_page_token' + ]); + + $response = MetaExtension::rest_update_fb_settings($request); + + // Assert that we get a WP_REST_Response with success=false + $this->assertInstanceOf(WP_REST_Response::class, $response); + $data = $response->get_data(); + $this->assertFalse($data['success']); + $this->assertNotEmpty($data['message']); + } + + /** + * Test generate_iframe_management_url + */ + public function test_generate_iframe_management_url() { + // Set up the access token + update_option('wc_facebook_access_token', 'test_access_token'); + + // Test with empty business ID (should return empty string) + $url = MetaExtension::generate_iframe_management_url(''); + $this->assertEmpty($url); + + // Test with valid business ID + $business_id = '123456789'; + $expected_url = 'https://www.facebook.com/commerce/app/management/123456789/'; + + // Add mock API response filter + $this->add_mock_api_response_filter($business_id); + + $url = MetaExtension::generate_iframe_management_url($business_id); + + // Remove our filter + remove_all_filters('pre_http_request'); + + $this->assertEquals($expected_url, $url); + } + + /** + * Test init_rest_endpoint registers the route + */ + public function test_init_rest_endpoint() { + // We need to run this in the context of the rest_api_init action + // to avoid the WordPress warning + add_action('rest_api_init', function() { + MetaExtension::init_rest_endpoint(); + }); + + // Trigger the rest_api_init action + do_action('rest_api_init'); + + // Check if the routes were registered + $routes = rest_get_server()->get_routes(); + + // Verify the update_fb_settings endpoint exists + $this->assertArrayHasKey('/wc-facebook/v1/update_fb_settings', $routes); + $this->assertArrayHasKey('POST', $routes['/wc-facebook/v1/update_fb_settings'][0]['methods']); + + // Just verify the permission callback and callback are set and callable + $this->assertTrue(isset($routes['/wc-facebook/v1/update_fb_settings'][0]['callback'])); + $this->assertTrue(isset($routes['/wc-facebook/v1/update_fb_settings'][0]['permission_callback'])); + + // Verify the uninstall endpoint exists + $this->assertArrayHasKey('/wc-facebook/v1/uninstall', $routes); + $this->assertArrayHasKey('POST', $routes['/wc-facebook/v1/uninstall'][0]['methods']); + + // Just verify the permission callback and callback are set and callable + $this->assertTrue(isset($routes['/wc-facebook/v1/uninstall'][0]['callback'])); + $this->assertTrue(isset($routes['/wc-facebook/v1/uninstall'][0]['permission_callback'])); + } + /** + * Helper method to add a filter that mocks the Facebook API response + * + * @param string $business_id The business ID to use in the mock response + */ + private function add_mock_api_response_filter($business_id) { + add_filter('pre_http_request', function($preempt, $args, $url) use ($business_id) { + // Check if this is the Facebook API call we want to mock + if (strpos($url, 'graph.facebook.com') !== false && + strpos($url, 'fbe_business') !== false) { + + return [ + 'body' => json_encode([ + 'commerce_extension' => [ + 'uri' => "https://www.facebook.com/commerce/app/management/{$business_id}/" + ] + ]), + 'response' => [ + 'code' => 200, + 'message' => 'OK' + ], + 'headers' => [], + 'cookies' => [], + 'filename' => null + ]; + } + + // Not our API call, let WordPress handle it + return $preempt; + }, 10, 3); + } +}