'; echo esc_html( $message ); echo '

'; } ); } endif; // Another PressNative plugin variant (e.g. legacy pressnative-engine) may already be active. // Bail early with an admin notice instead of triggering a class redeclaration fatal error. if ( class_exists( 'PressNative_Options', false ) ) { pressnative_fail_safe( __( 'PressNative Apps could not load because another PressNative plugin is already active. Deactivate and remove the other plugin folder (for example pressnative-engine), then activate PressNative Apps again.', 'pressnative-apps' ) ); return; } if ( version_compare( PHP_VERSION, '7.4', '<' ) ) { pressnative_fail_safe( sprintf( /* translators: %s: current PHP version */ __( 'PressNative Apps requires PHP 7.4 or higher. This site is running PHP %s. The rest of the site is still running.', 'pressnative-apps' ), PHP_VERSION ) ); return; } define( 'PRESSNATIVE_VERSION', '1.3.2' ); define( 'PRESSNATIVE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PRESSNATIVE_PLUGIN_FILE', __FILE__ ); $pressnative_missing = array(); foreach ( pressnative_required_files() as $pressnative_rel ) { if ( ! is_readable( PRESSNATIVE_PLUGIN_DIR . $pressnative_rel ) ) { $pressnative_missing[] = $pressnative_rel; } } if ( ! empty( $pressnative_missing ) ) { pressnative_fail_safe( sprintf( /* translators: %s: comma-separated file list */ __( 'PressNative Apps is missing required files (%s), usually after a failed plugin update. The rest of the site is still running. Reinstall PressNative Apps from a zip, or restore the previous version from your host backup.', 'pressnative-apps' ), implode( ', ', $pressnative_missing ) ) ); unset( $pressnative_missing, $pressnative_rel ); return; } unset( $pressnative_missing, $pressnative_rel ); foreach ( pressnative_required_files() as $pressnative_rel ) { require_once PRESSNATIVE_PLUGIN_DIR . $pressnative_rel; } unset( $pressnative_rel ); /** * Activation: create devices table. Registry contact happens only after the site owner connects (API key). */ register_activation_hook( __FILE__, function () { PressNative_Devices::create_table(); // Schedule async AOT warm-up so existing content is pre-compiled. PressNative_AOT_Compiler::schedule_warmup(); // Register .well-known rewrite rules. PressNative_Well_Known::flush_rewrite_rules(); } ); /** * Deactivation: currently a no-op. Data is preserved so reactivation is seamless. * Full cleanup (options + database table) happens in uninstall.php. */ register_deactivation_hook( __FILE__, function () { // Stop any in-progress AOT batch compilation. wp_clear_scheduled_hook( PressNative_AOT_Compiler::CRON_HOOK ); // Data is preserved for reactivation. See uninstall.php for full cleanup. } ); /** * REST API: layout/home and register-device. */ add_action( 'rest_api_init', function () { try { $layout = new PressNative_Layout(); register_rest_route( 'pressnative/v1', '/layout/home', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $data = $layout->get_home_layout(); $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data ); if ( ! is_wp_error( $response ) && $response->get_status() === 200 ) { $device_id = $request->get_param( 'device_id' ); PressNative_Analytics::forward_event_to_registry( 'home', 'home', get_bloginfo( 'name' ), null, $device_id ); } return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', ) ); register_rest_route( 'pressnative/v1', '/layout/posts', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $page = (int) $request->get_param( 'page' ); $per_page = (int) $request->get_param( 'per_page' ); if ( $page < 1 ) { $page = 1; } if ( $per_page < 1 || $per_page > 50 ) { $per_page = 20; } $data = $layout->get_posts_list_layout( $page, $per_page ); $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data ); return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', 'args' => array( 'page' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'sanitize_callback' => 'absint', ), 'per_page' => array( 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 50, 'sanitize_callback' => 'absint', ), ), ) ); register_rest_route( 'pressnative/v1', '/layout/post/(?P[\d]+)', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $post_id = (int) $request['id']; // AOT cache: read pre-compiled SDUI blocks from post meta. // Falls back to on-demand DOM parsing when the cache is empty. $cached_blocks = PressNative_AOT_Compiler::get_cached_blocks( $post_id ); $data = $layout->get_post_layout( $post_id, $cached_blocks ); if ( ! $data ) { return new WP_Error( 'not_found', 'Post not found', array( 'status' => 404 ) ); } $post = get_post( $post_id ); $modified = ( $post instanceof WP_Post ) ? $post->post_modified_gmt : null; $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data, $modified ); if ( ! is_wp_error( $response ) && $response->get_status() === 200 ) { $title = isset( $data['screen']['title'] ) ? $data['screen']['title'] : get_the_title( $post_id ); $device_id = $request->get_param( 'device_id' ); PressNative_Analytics::forward_event_to_registry( 'post', (string) $post_id, $title, null, $device_id ); } return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', ) ); register_rest_route( 'pressnative/v1', '/layout/page/(?P[a-zA-Z0-9\-_]+)', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $slug = $request['slug']; $data = $layout->get_page_layout( $slug ); if ( ! $data ) { return new WP_Error( 'not_found', 'Page not found', array( 'status' => 404 ) ); } $page_post = get_page_by_path( $slug ); $modified = ( $page_post instanceof WP_Post ) ? $page_post->post_modified_gmt : null; $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data, $modified ); if ( ! is_wp_error( $response ) && $response->get_status() === 200 ) { $title = isset( $data['screen']['title'] ) ? $data['screen']['title'] : ''; $device_id = $request->get_param( 'device_id' ); PressNative_Analytics::forward_event_to_registry( 'page', $slug, $title, null, $device_id ); } return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', ) ); register_rest_route( 'pressnative/v1', '/layout/category/(?P[\d]+)', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $data = $layout->get_category_layout( (int) $request['id'] ); if ( ! $data ) { return new WP_Error( 'not_found', 'Category not found', array( 'status' => 404 ) ); } $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data ); if ( ! is_wp_error( $response ) && $response->get_status() === 200 ) { $title = isset( $data['screen']['title'] ) ? $data['screen']['title'] : ''; $device_id = $request->get_param( 'device_id' ); PressNative_Analytics::forward_event_to_registry( 'category', (string) $request['id'], $title, null, $device_id ); } return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', ) ); // WooCommerce: shop, product, product-category layouts. if ( PressNative_WooCommerce::is_active() ) { register_rest_route( 'pressnative/v1', '/layout/shop', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $data = $layout->get_shop_layout(); if ( ! $data ) { return new WP_Error( 'not_found', 'Shop not available', array( 'status' => 404 ) ); } $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data ); if ( ! is_wp_error( $response ) && $response->get_status() === 200 ) { $device_id = $request->get_param( 'device_id' ); PressNative_Analytics::forward_event_to_registry( 'shop', 'shop', isset( $data['screen']['title'] ) ? $data['screen']['title'] : 'Shop', null, $device_id ); } return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', ) ); register_rest_route( 'pressnative/v1', '/layout/product/(?P[\d]+)', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $product_id = (int) $request['id']; $data = $layout->get_product_layout( $product_id ); if ( ! $data ) { return new WP_Error( 'not_found', 'Product not found', array( 'status' => 404 ) ); } $product = get_post( $product_id ); $modified = ( $product instanceof WP_Post ) ? $product->post_modified_gmt : null; $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data, $modified ); if ( ! is_wp_error( $response ) && $response->get_status() === 200 ) { $title = isset( $data['screen']['title'] ) ? $data['screen']['title'] : ''; $device_id = $request->get_param( 'device_id' ); PressNative_Analytics::forward_event_to_registry( 'product', (string) $product_id, $title, null, $device_id ); } return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', ) ); register_rest_route( 'pressnative/v1', '/layout/product-category/(?P[\d]+)', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $data = $layout->get_product_category_layout( (int) $request['id'] ); if ( ! $data ) { return new WP_Error( 'not_found', 'Product category not found', array( 'status' => 404 ) ); } $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data ); if ( ! is_wp_error( $response ) && $response->get_status() === 200 ) { $title = isset( $data['screen']['title'] ) ? $data['screen']['title'] : ''; $device_id = $request->get_param( 'device_id' ); PressNative_Analytics::forward_event_to_registry( 'product_category', (string) $request['id'], $title, null, $device_id ); } return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', ) ); } // Demo/setup: apply a launch kit (publisher, shop, local) via REST for automated site polish. register_rest_route( 'pressnative/v1', '/setup/launch-kit', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( WP_REST_Request $request ) { $kit_id = sanitize_key( (string) $request->get_param( 'kit_id' ) ); if ( $kit_id === '' ) { $kit_id = 'publisher'; } $result = PressNative_Launch_Kits::apply_kit( $kit_id ); if ( is_wp_error( $result ) ) { return $result; } if ( class_exists( 'PressNative_Registry_Notify' ) ) { PressNative_Registry_Notify::notify_if_configured(); } return rest_ensure_response( array( 'ok' => true, 'kit' => $kit_id, ) ); }, 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'kit_id' => array( 'type' => 'string', 'default' => 'publisher', 'sanitize_callback' => 'sanitize_key', ), ), ) ); // Branding GET endpoint: Public API for clients to fetch app branding (app name, logo, theme colors). register_rest_route( 'pressnative/v1', '/branding', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { return rest_ensure_response( PressNative_Options::get_branding() ); }, // Public: app theme (name, colors, logo) the apps need before any login. 'permission_callback' => '__return_true', ) ); // Branding sync endpoint: Registry pushes branding updates to WordPress. register_rest_route( 'pressnative/v1', '/branding', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( WP_REST_Request $request ) { $params = $request->get_json_params(); if ( empty( $params ) || ! is_array( $params ) ) { return new WP_Error( 'invalid_body', 'JSON body required', array( 'status' => 400 ) ); } $map = array( 'app_name' => PressNative_Options::OPTION_APP_NAME, 'primary_color' => PressNative_Options::OPTION_PRIMARY_COLOR, 'accent_color' => PressNative_Options::OPTION_ACCENT_COLOR, 'background_color' => PressNative_Options::OPTION_BACKGROUND_COLOR, 'text_color' => PressNative_Options::OPTION_TEXT_COLOR, 'border_color' => PressNative_Options::OPTION_BORDER_COLOR, 'font_family' => PressNative_Options::OPTION_FONT_FAMILY, 'base_font_size' => PressNative_Options::OPTION_BASE_FONT_SIZE, ); $updated = array(); foreach ( $map as $key => $option_name ) { if ( isset( $params[ $key ] ) ) { $value = $params[ $key ]; if ( in_array( $key, array( 'primary_color', 'accent_color', 'background_color', 'text_color', 'border_color' ), true ) ) { $value = PressNative_Options::sanitize_hex( $value ); } elseif ( 'base_font_size' === $key ) { $value = max( 12, min( 28, absint( $value ) ) ); } else { $value = sanitize_text_field( (string) $value ); } update_option( $option_name, $value ); $updated[] = $key; } } // Handle logo_url: download to media library if it's external. if ( isset( $params['logo_url'] ) && ! empty( $params['logo_url'] ) ) { $logo_url = esc_url_raw( $params['logo_url'] ); $current = (int) get_option( PressNative_Options::OPTION_LOGO_ATTACHMENT, 0 ); $current_u = $current > 0 ? wp_get_attachment_image_url( $current, 'full' ) : ''; if ( $logo_url !== $current_u ) { require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $attachment_id = media_sideload_image( $logo_url, 0, 'PressNative Logo', 'id' ); if ( ! is_wp_error( $attachment_id ) ) { update_option( PressNative_Options::OPTION_LOGO_ATTACHMENT, $attachment_id ); $updated[] = 'logo_url'; } } } return rest_ensure_response( array( 'ok' => true, 'updated' => $updated, ) ); }, 'permission_callback' => array( 'PressNative_Admin', 'permission_check_admin_or_registry' ), ) ); // Site info endpoint: Registry fetches admin email and site metadata for the admin dashboard. register_rest_route( 'pressnative/v1', '/site-info', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { $app_name = (string) get_option( PressNative_Options::OPTION_APP_NAME, get_bloginfo( 'name' ) ); return rest_ensure_response( array( 'admin_email' => (string) get_option( 'admin_email', '' ), 'site_name' => $app_name, 'blog_name' => get_bloginfo( 'name' ), 'language' => get_bloginfo( 'language' ), 'timezone' => wp_timezone_string(), ) ); }, 'permission_callback' => array( 'PressNative_Admin', 'permission_check_admin_or_registry' ), ) ); // Site verification endpoint: Registry calls this to confirm wp-admin access. // The plugin generates a nonce (see PressNative_Admin::trigger_site_verification), // sends it to the Registry, and the Registry calls back here to confirm it. register_rest_route( 'pressnative/v1', '/verify-ownership', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => function () { // Nonce was validated in permission_callback — delete it (single use) and confirm. delete_transient( 'pressnative_verify_nonce' ); return rest_ensure_response( array( 'verified' => true, 'site_url' => home_url(), ) ); }, 'permission_callback' => array( 'PressNative_Admin', 'permission_check_verify_ownership' ), ) ); // Preview endpoint: returns home layout with optional overrides (no save). register_rest_route( 'pressnative/v1', '/preview', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $overrides = $request->get_json_params(); if ( ! is_array( $overrides ) ) { $overrides = array(); } $callbacks = PressNative_Preview::apply_overrides( $overrides ); try { $data = $layout->get_home_layout(); return rest_ensure_response( $data ); } finally { PressNative_Preview::remove_overrides( $callbacks ); } }, 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, ) ); // Documentation endpoint: returns documentation layout for the mobile apps. register_rest_route( 'pressnative/v1', '/layout/documentation', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) use ( $layout ) { $data = $layout->get_documentation_layout(); $response = rest_ensure_response( $data ); $response->header( 'X-PressNative-Version', PRESSNATIVE_VERSION ); $response = PressNative_Http_Cache::apply( $request, $response, $data ); if ( ! is_wp_error( $response ) && $response->get_status() === 200 ) { $device_id = $request->get_param( 'device_id' ); PressNative_Analytics::forward_event_to_registry( 'documentation', 'documentation', 'PressNative Documentation', null, $device_id ); } return $response; }, // Public: read-only published content for the mobile apps. 'permission_callback' => '__return_true', ) ); // WooCommerce: native add-to-cart and cart count (Store API proxy). if ( PressNative_WooCommerce::is_active() ) { register_rest_route( 'pressnative/v1', '/cart/add', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( WP_REST_Request $request ) { $product_id = (int) $request->get_param( 'product_id' ); $variation_id = $request->get_param( 'variation_id' ); $quantity = (int) $request->get_param( 'quantity' ); if ( $quantity < 1 ) { $quantity = 1; } if ( $product_id < 1 ) { return new WP_Error( 'invalid_product', 'product_id required', array( 'status' => 400 ) ); } if ( ! function_exists( 'WC' ) ) { return new WP_Error( 'woocommerce_unavailable', 'WooCommerce not available', array( 'status' => 503 ) ); } if ( ! WC()->cart ) { wc_load_cart(); // For WooCommerce 9.0+, also load cart from session if ( method_exists( WC()->cart, 'get_cart_from_session' ) ) { WC()->cart->get_cart_from_session(); } } if ( ! WC()->cart ) { return new WP_Error( 'woocommerce_unavailable', 'Cart not available', array( 'status' => 503 ) ); } $device_id = $request->get_param( 'device_id' ); if ( empty( $device_id ) ) { $device_id = $request->get_header( 'X-Device-Id' ); } if ( is_string( $device_id ) && '' !== $device_id && class_exists( 'PressNative_Cart_Recovery' ) ) { PressNative_Cart_Recovery::bind_device( $device_id ); } $variation_id = is_numeric( $variation_id ) ? (int) $variation_id : 0; $added = WC()->cart->add_to_cart( $product_id, $quantity, $variation_id ); if ( $added === false ) { return new WP_Error( 'add_failed', 'Could not add to cart', array( 'status' => 400 ) ); } $count = WC()->cart->get_cart_contents_count(); return rest_ensure_response( array( 'ok' => true, 'cart_count' => $count, ) ); }, 'permission_callback' => array( 'PressNative_WooCommerce', 'permission_check_cart_mutate' ), 'args' => array( 'product_id' => array( 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ), 'variation_id' => array( 'required' => false, 'type' => 'integer', ), 'quantity' => array( 'required' => false, 'type' => 'integer', 'default' => 1, ), 'device_id' => array( 'required' => false, 'type' => 'string', ), ), ) ); register_rest_route( 'pressnative/v1', '/cart/remove', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( $request ) { $product_id = (int) $request->get_param( 'product_id' ); if ( $product_id <= 0 ) { return new WP_Error( 'invalid_product', 'Invalid product ID', array( 'status' => 400 ) ); } if ( ! function_exists( 'WC' ) ) { return new WP_Error( 'woocommerce_unavailable', 'WooCommerce not available', array( 'status' => 503 ) ); } if ( ! WC()->cart ) { wc_load_cart(); if ( method_exists( WC()->cart, 'get_cart_from_session' ) ) { WC()->cart->get_cart_from_session(); } } if ( ! WC()->cart ) { return new WP_Error( 'woocommerce_unavailable', 'Cart not available', array( 'status' => 503 ) ); } // Find and remove the cart item $removed = false; foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { if ( (int) $cart_item['product_id'] === $product_id ) { WC()->cart->remove_cart_item( $cart_item_key ); $removed = true; break; // Remove only the first match } } if ( ! $removed ) { return new WP_Error( 'item_not_found', 'Item not found in cart', array( 'status' => 404 ) ); } $count = WC()->cart->get_cart_contents_count(); return rest_ensure_response( array( 'ok' => true, 'cart_count' => $count, ) ); }, 'permission_callback' => array( 'PressNative_WooCommerce', 'permission_check_cart_mutate' ), 'args' => array( 'product_id' => array( 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), ) ); register_rest_route( 'pressnative/v1', '/cart/update', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => function ( $request ) { $product_id = (int) $request->get_param( 'product_id' ); $quantity = (int) $request->get_param( 'quantity' ); if ( $product_id <= 0 ) { return new WP_Error( 'invalid_product', 'Invalid product ID', array( 'status' => 400 ) ); } if ( $quantity < 0 ) { return new WP_Error( 'invalid_quantity', 'Quantity must be 0 or greater', array( 'status' => 400 ) ); } if ( ! function_exists( 'WC' ) ) { return new WP_Error( 'woocommerce_unavailable', 'WooCommerce not available', array( 'status' => 503 ) ); } if ( ! WC()->cart ) { wc_load_cart(); if ( method_exists( WC()->cart, 'get_cart_from_session' ) ) { WC()->cart->get_cart_from_session(); } } if ( ! WC()->cart ) { return new WP_Error( 'woocommerce_unavailable', 'Cart not available', array( 'status' => 503 ) ); } // Find and update the cart item $updated = false; foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { if ( (int) $cart_item['product_id'] === $product_id ) { if ( $quantity === 0 ) { // Remove item if quantity is 0 WC()->cart->remove_cart_item( $cart_item_key ); } else { // Update quantity WC()->cart->set_quantity( $cart_item_key, $quantity ); } $updated = true; break; // Update only the first match } } if ( ! $updated ) { return new WP_Error( 'item_not_found', 'Item not found in cart', array( 'status' => 404 ) ); } $count = WC()->cart->get_cart_contents_count(); return rest_ensure_response( array( 'ok' => true, 'cart_count' => $count, ) ); }, 'permission_callback' => array( 'PressNative_WooCommerce', 'permission_check_cart_mutate' ), 'args' => array( 'product_id' => array( 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ), 'quantity' => array( 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), ) ); register_rest_route( 'pressnative/v1', '/cart/count', array( 'methods' => WP_REST_Server::READABLE, 'callback' => function () { if ( ! function_exists( 'WC' ) ) { return rest_ensure_response( array( 'cart_count' => 0 ) ); } if ( ! WC()->cart ) { wc_load_cart(); // For WooCommerce 9.0+, also load cart from session if ( method_exists( WC()->cart, 'get_cart_from_session' ) ) { WC()->cart->get_cart_from_session(); } } if ( ! WC()->cart ) { return rest_ensure_response( array( 'cart_count' => 0 ) ); } $cart_items = array(); foreach ( WC()->cart->get_cart() as $key => $item ) { $product_details = PressNative_WooCommerce::get_product( (int) $item['product_id'] ); $cart_items[] = array( 'product_id' => (int) $item['product_id'], 'quantity' => (int) $item['quantity'], 'name' => $product_details['title'] ?? '', 'price' => $product_details['price'] ?? '', 'image' => $product_details['image_url'] ?? '', ); } return rest_ensure_response( array( 'cart_count' => WC()->cart->get_cart_contents_count(), 'cart_items' => $cart_items, ) ); }, // Public: reads only the caller's own WooCommerce session cart. 'permission_callback' => '__return_true', ) ); } PressNative_Devices::register_rest_route(); PressNative_Analytics::register_rest_routes(); PressNative_Search_Api::register_routes(); PressNative_Well_Known::register_rest_routes(); } catch ( Throwable $e ) { pressnative_fail_safe( sprintf( /* translators: %s: exception message */ __( 'PressNative Apps could not register its API (%s). The rest of the site is still running.', 'pressnative-apps' ), $e->getMessage() ) ); } } ); /** * Minimal template for in-app WebView navigations. * * When a same-origin link is followed inside the app's WebView, the request * includes ?pressnative=1. This hook intercepts such requests and renders a * minimal HTML page containing only the post/page content with all enqueued * scripts and styles — no theme header, footer, sidebars or navigation. */ add_action( 'template_redirect', function () { try { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- URL param from app WebView, not a form. $pn = isset( $_GET['pressnative'] ) ? sanitize_text_field( wp_unslash( $_GET['pressnative'] ) ) : ''; if ( $pn !== '1' ) { return; } // Let WordPress determine the queried object normally. $post = get_queried_object(); if ( ! ( $post instanceof WP_Post ) ) { // Fallback: try the global $post. global $post; } if ( ! $post || ! ( $post instanceof WP_Post ) ) { status_header( 404 ); echo '

Not found

'; exit; } // Set up post data so shortcodes and the_content work. setup_postdata( $post ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WordPress filter $content = apply_filters( 'the_content', $post->post_content ); // Enqueue content styles for WebView (output in wp_head). wp_register_style( 'pressnative-webview-content', false, array(), PRESSNATIVE_VERSION ); wp_add_inline_style( 'pressnative-webview-content', 'body{font-family:sans-serif;font-size:16px;line-height:1.5;margin:0;padding:16px;}img{max-width:100%;height:auto;}table{width:100%;border-collapse:collapse;}' ); wp_enqueue_style( 'pressnative-webview-content' ); // Capture the full output so we can relativize absolute site URLs. // This ensures script/style URLs work in the mobile WebView which may // access the site via a different host (e.g. 10.0.2.2 on Android emulator). ob_start(); ?> > getMessage() ) ); } } ); try { /** * Admin: PressNative menu and Registry URL setting. */ PressNative_Admin::init(); /** * QR code shortcode for app deep links. */ PressNative_QR::init(); /** * AOT compiler: pre-build SDUI JSON on every post/page/product save. */ PressNative_AOT_Compiler::init(); /** * Notify Registry when branding/layout options are saved (invalidates site branding cache). */ PressNative_Registry_Notify::init(); /** * Serve .well-known app association files for Universal / App Links. */ PressNative_Well_Known::init(); /** * Editorial push metabox (notify on publish). */ PressNative_Editorial_Push::init(); /** * WooCommerce abandoned-cart / back-in-stock push. */ PressNative_Cart_Recovery::init(); /** * One-time option migrations after an upgrade. */ add_action( 'admin_init', array( 'PressNative_Options', 'maybe_migrate' ) ); } catch ( Throwable $e ) { pressnative_fail_safe( sprintf( /* translators: %s: exception message */ __( 'PressNative Apps failed to start (%s). The rest of the site is still running.', 'pressnative-apps' ), $e->getMessage() ) ); return; } /** * Whether the current WooCommerce page was opened from the app's checkout handoff. * Display-only: decides whether to show a "Return to app" link; no state changes. * * @return bool */ function pressnative_is_app_checkout_request() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only flag, no state change if ( isset( $_GET['pressnative_app_checkout'] ) || isset( $_GET['pressnative_checkout_token'] ) ) { return true; } $referer = wp_get_referer(); return $referer && false !== strpos( $referer, 'pressnative_checkout_token=' ); } /** * Handle checkout completion and return to app. */ add_action( 'woocommerce_thankyou', function ( $order_id ) { if ( ! $order_id ) { return; } if ( ! pressnative_is_app_checkout_request() ) { return; } // Enqueue script to redirect to app after a short delay. wp_enqueue_script( 'jquery' ); wp_enqueue_script( 'pressnative-return-to-app', false, array( 'jquery' ), PRESSNATIVE_VERSION, true ); wp_add_inline_script( 'pressnative-return-to-app', '(function(){var o=' . (int) $order_id . ';var u="app.pressnative.hub://checkout-complete?order_id="+o;setTimeout(function(){var b=document.createElement("button");b.innerHTML="Return to App";b.style.cssText="background:#0073aa;color:white;padding:12px 24px;border:none;border-radius:4px;font-size:16px;margin:20px 0;cursor:pointer";b.onclick=function(){window.location.href=u};var d=document.querySelector(".woocommerce-order");(d||document.body).appendChild(b)},1000);setTimeout(function(){window.location.href=u},10000)})();', 'after' ); }, 10, 1 ); /** * Add return to app link on cart page when accessed from app. */ add_action( 'woocommerce_after_cart', function () { if ( ! pressnative_is_app_checkout_request() ) { return; } ?> getMessage() ) ); } /** * Register app download shortcodes. */ add_action( 'init', function () { add_shortcode( 'pressnative_download', function ( $atts ) { $atts = shortcode_atts( array( 'style' => 'both', // 'ios', 'android', 'both' 'size' => 'medium', // 'small', 'medium', 'large' 'text' => '', // Custom text override 'ios_url' => '', // Custom iOS URL 'android_url' => '', // Custom Android URL ), $atts, 'pressnative_download' ); $site_url = untrailingslashit( home_url() ); // Generate fallback HTML for web browsers with deferred deep linking $html = '
'; if ( $atts['style'] === 'ios' || $atts['style'] === 'both' ) { $ios_base_url = $atts['ios_url'] ? esc_url_raw( $atts['ios_url'] ) : 'https://apps.apple.com/app/pressnative/id0000000000'; $ios_url = $ios_base_url . '?pt=pressnative&ct=' . rawurlencode( $site_url ); $html .= '📱 Download for iOS'; } if ( $atts['style'] === 'android' || $atts['style'] === 'both' ) { $android_base_url = $atts['android_url'] ? esc_url_raw( $atts['android_url'] ) : 'https://play.google.com/store/apps/details?id=app.pressnative.hub'; $android_url = $android_base_url . '&referrer=utm_source%3Dpressnative%26utm_content%3D' . rawurlencode( $site_url ); $html .= '🤖 Download for Android'; } $html .= '
'; return wp_kses_post( $html ); } ); // Debug shortcode for testing deep links add_shortcode( 'pressnative_debug_links', function () { if ( ! current_user_can( 'manage_options' ) ) { return ''; // Only show to admins } $site_url = untrailingslashit( home_url() ); $deep_link_url = 'https://pressnative.app/open?site=' . rawurlencode( $site_url ); $custom_scheme_url = 'app.pressnative.hub://site?url=' . rawurlencode( $site_url ); $allowed_protocols = array_merge( wp_allowed_protocols(), array( 'app.pressnative.hub' ) ); $html = '
'; $html .= '

🔧 PressNative Debug Links (Admin Only)

'; $html .= '

Deep Link (HTTPS):
' . esc_html( $deep_link_url ) . '

'; $html .= '

Custom Scheme:
' . esc_html( $custom_scheme_url ) . '

'; $html .= '

Click these links on your mobile device to test deep linking.

'; $html .= '
'; return wp_kses( $html, wp_kses_allowed_html( 'post' ), $allowed_protocols ); } ); } );