array( 'privacy-policy', 'privacy', 'legal/privacy' ), 'terms' => array( 'terms-of-service', 'terms', 'tos', 'legal/terms' ), 'about' => array( 'about', 'about-us' ), 'contact' => array( 'contact', 'contact-us' ), 'shipping' => array( 'shipping', 'shipping-policy', 'shipping-and-returns' ), ); public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } public static function tab_url( $tab = 'general', $extra = array() ) { $args = array_merge( array( 'page' => 'agentic-commerce-for-woocommerce', 'tab' => $tab, ), $extra ); return add_query_arg( $args, admin_url( 'options-general.php' ) ); } private function __construct() { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_init', array( $this, 'maybe_capture_partner_from_query' ) ); add_action( 'admin_post_xpay_wc_connect_start', array( $this, 'handle_connect_start' ) ); add_action( 'admin_post_xpay_wc_disconnect', array( $this, 'handle_disconnect' ) ); add_action( 'admin_post_xpay_wc_audit', array( $this, 'handle_audit' ) ); add_action( 'admin_post_xpay_wc_telemetry', array( $this, 'handle_telemetry_toggle' ) ); add_action( 'admin_post_xpay_wc_save_partner', array( $this, 'handle_save_partner' ) ); add_action( 'admin_post_xpay_wc_save_capabilities', array( $this, 'handle_save_capabilities' ) ); add_action( 'admin_post_xpay_wc_save_payments', array( $this, 'handle_save_payments' ) ); add_action( 'admin_post_xpay_wc_save_links', array( $this, 'handle_save_links' ) ); add_action( 'admin_post_xpay_wc_telemetry_debug', array( $this, 'handle_telemetry_debug_toggle' ) ); add_action( 'admin_post_xpay_wc_refresh_catalog', array( $this, 'handle_refresh_catalog' ) ); add_action( 'admin_post_xpay_wc_test_connection', array( $this, 'handle_test_connection' ) ); add_action( 'admin_post_xpay_wc_run_diagnostics', array( $this, 'handle_run_diagnostics' ) ); add_action( 'rest_api_init', array( $this, 'register_finalize_route' ) ); add_filter( 'plugin_action_links_' . plugin_basename( XPAY_WC_FILE ), array( $this, 'plugin_action_links' ) ); add_action( 'xpay_wc_initial_resync', array( $this, 'cron_initial_resync' ), 10, 1 ); } /** * Cron handler — fires the initial catalog resync out-of-band so the REST * finalize response stays under WordPress's external-request timeout. */ public function cron_initial_resync( $slug ) { if ( ! $slug || ! is_string( $slug ) ) { return; } Xpay_Client::post( '/v1/merchants/' . $slug . '/resync', array( 'reason' => 'initial_install' ) ); } public function register_menu() { add_options_page( __( 'xpay', 'agentic-commerce-for-woocommerce' ), __( 'xpay', 'agentic-commerce-for-woocommerce' ), 'manage_woocommerce', 'agentic-commerce-for-woocommerce', array( $this, 'render_page' ) ); } public function plugin_action_links( $links ) { array_unshift( $links, sprintf( '%s', esc_url( self::tab_url() ), esc_html__( 'Settings', 'agentic-commerce-for-woocommerce' ) ) ); return $links; } public function register_finalize_route() { register_rest_route( 'xpay/v1', '/finalize', array( 'methods' => 'POST', 'callback' => array( $this, 'rest_finalize' ), // Authentication is performed inside rest_finalize() itself: the // payload-supplied nonce is compared via hash_equals() against the // single-use option set in handle_connect_start(), and the option // is deleted on first success. This is the OAuth-callback pattern // — no WP user is signed in when xpay calls back, so a capability // check is not possible here; the nonce IS the auth. 'permission_callback' => '__return_true', ) ); } public function rest_finalize( WP_REST_Request $req ) { $payload = $req->get_json_params(); $nonce = isset( $payload['nonce'] ) ? sanitize_text_field( $payload['nonce'] ) : ''; $slug = isset( $payload['merchant_slug'] ) ? sanitize_title( $payload['merchant_slug'] ) : ''; $api_key = isset( $payload['api_key'] ) ? sanitize_text_field( $payload['api_key'] ) : ''; if ( ! $slug || ! $api_key ) { Xpay_Telemetry::track( 'finalize_error', array( 'reason' => 'missing_fields' ) ); return new WP_REST_Response( array( 'error' => 'missing_fields' ), 400 ); } $stored = get_option( self::NONCE_OPTION ); $existing_slug = get_option( 'xpay_wc_merchant_slug' ); $existing_key = get_option( 'xpay_wc_api_key' ); // Idempotent replay path: the backend may re-send the same creds if its // first callback's HTTP response was lost (network/lambda timeout). If // the slug + api_key already match what we have stored, accept silently. $is_replay = $existing_slug && $existing_key && hash_equals( (string) $existing_slug, $slug ) && hash_equals( (string) $existing_key, $api_key ); if ( ! $is_replay ) { // Concurrent-OAuth-callback race: WC's server-side wc-auth POST and // the browser-return path can both fire xpay's wcAuthCallback Lambda // within ~500ms of each other. Each invocation mints a fresh api_key // (pre-v0.3.2-backend behavior), so the second call arrives with a // different api_key + the same nonce. The first call already burned // the nonce; this second one fails is_replay (api_keys differ) and // fails nonce match (nonce gone). Without this guard it 401s and the // backend marks the merchant as plugin_callback_failed even though // the plugin actually finalized cleanly on call #1. // // If creds are already stored (any slug + any api_key), treat the // incoming request as a duplicate callback. Return 200 with the // EXISTING slug so the orchestration upstream stays consistent. We // never overwrite stored creds on this path — the first finalize // won, and that's the truth. if ( $existing_slug && $existing_key ) { Xpay_Telemetry::track( 'finalize_duplicate_callback', array( 'slug' => $existing_slug ) ); return new WP_REST_Response( array( 'ok' => true, 'already_finalized' => true, 'slug' => (string) $existing_slug, 'site_token' => (string) get_option( 'xpay_wc_site_token', '' ), ), 200 ); } if ( ! $nonce || ! $stored || ! hash_equals( $stored, $nonce ) ) { Xpay_Telemetry::track( 'finalize_error', array( 'reason' => 'invalid_nonce' ) ); return new WP_REST_Response( array( 'error' => 'invalid_nonce' ), 401 ); } } update_option( 'xpay_wc_merchant_slug', $slug ); update_option( 'xpay_wc_api_key', $api_key ); update_option( 'xpay_wc_connected_at', time() ); // Clear the pending-attempt marker — the handshake succeeded. delete_option( 'xpay_wc_last_connect_attempt' ); // Persist the UCP business profile body if the backend sent one. // Served at /.well-known/ucp by Xpay_REST::serve_ucp_profile(). if ( isset( $payload['ucp_profile'] ) && is_array( $payload['ucp_profile'] ) ) { $encoded = wp_json_encode( $payload['ucp_profile'] ); if ( false !== $encoded ) { update_option( 'xpay_wc_ucp_profile', $encoded ); } } if ( isset( $payload['ucp_signing_keys'] ) && is_array( $payload['ucp_signing_keys'] ) ) { update_option( 'xpay_wc_ucp_signing_keys', $payload['ucp_signing_keys'] ); } // Burn the local nonce only after creds are safely stored. // (Skipped on replay — the nonce may already be gone from a prior success.) if ( ! $is_replay ) { delete_option( self::NONCE_OPTION ); } Xpay_Telemetry::track( 'finalize_success', array( 'slug' => $slug, 'replay' => $is_replay ? 1 : 0, ) ); // Kick the first catalog sync asynchronously (don't block the response, // don't fail finalize if resync errors). Scheduling a single-event cron // is the simplest fire-and-forget that survives request-end on most WP // hosts. Falls back to an inline non-blocking call if cron is disabled. if ( ! $is_replay ) { if ( ! wp_next_scheduled( 'xpay_wc_initial_resync' ) ) { wp_schedule_single_event( time() + 1, 'xpay_wc_initial_resync', array( $slug ) ); } // Best-effort inline trigger in case wp-cron is disabled. Non-blocking // `wp_remote_request` returns immediately after the TCP write — perfect // for keeping the REST response fast. wp_remote_post( trailingslashit( XPAY_WC_API_BASE ) . 'v1/merchants/' . rawurlencode( $slug ) . '/resync', array( 'timeout' => 0.5, 'blocking' => false, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key, 'X-Xpay-Site' => home_url( '/' ), ), 'body' => wp_json_encode( array( 'reason' => 'initial_install' ) ), ) ); } // Ship the site_token back as part of the finalize handshake response // so the backend can capture and persist it server-to-server, without // it ever appearing in a URL parameter, browser history, or access // log. The backend stores this on the merchant record and uses it as // the X-Xpay-Site-Token header for any future /wp-json/xpay/v1/admin/refresh // call. Idempotent: same value on every finalize (including replays). return new WP_REST_Response( array( 'ok' => true, 'replay' => $is_replay, 'site_token' => (string) get_option( 'xpay_wc_site_token', '' ), ), 200 ); } public function handle_disconnect() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_disconnect' ); $slug = Xpay_Plugin::merchant_slug(); $api_key = Xpay_Plugin::api_key(); Xpay_Telemetry::track( 'disconnected', array( 'slug' => $slug ) ); // Tell the backend BEFORE we clear local creds. Non-blocking, fire-and-forget — // the local disconnect must succeed even if the backend is unreachable. The // backend marks the merchant row status='disconnected' and archives the S3 catalog. if ( $slug && $api_key ) { wp_remote_request( trailingslashit( XPAY_WC_API_BASE ) . 'v1/merchants/' . rawurlencode( $slug ), array( 'method' => 'DELETE', 'timeout' => 0.5, 'blocking' => false, 'headers' => array( 'Accept' => 'application/json', 'X-Xpay-Api-Key' => $api_key, 'X-Xpay-Site' => home_url( '/' ), ), ) ); } delete_option( 'xpay_wc_merchant_slug' ); delete_option( 'xpay_wc_api_key' ); delete_option( 'xpay_wc_connected_at' ); delete_option( 'xpay_wc_last_audit' ); delete_option( 'xpay_wc_ucp_profile' ); delete_option( 'xpay_wc_ucp_signing_keys' ); delete_option( 'xpay_wc_payment_map' ); delete_option( 'xpay_wc_links' ); delete_option( 'xpay_wc_telemetry_debug' ); foreach ( self::CAPABILITIES as $cap ) { delete_option( 'xpay_wc_capability_' . $cap ); } wp_safe_redirect( self::tab_url( 'general', array( 'disconnected' => 1 ) ) ); exit; } public function handle_audit() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_audit' ); Xpay_Telemetry::track( 'audit_rerun_clicked' ); $result = Xpay_Client::post( '/v1/audits/run', array( 'url' => home_url( '/' ), 'slug' => Xpay_Plugin::merchant_slug(), 'email' => wp_get_current_user()->user_email, ), 20 ); if ( ! is_wp_error( $result ) ) { update_option( 'xpay_wc_last_audit', $result ); Xpay_Telemetry::track( 'audit_rerun_success', array( 'score' => is_array( $result ) && isset( $result['score'] ) ? (int) $result['score'] : null, ) ); } else { Xpay_Telemetry::track( 'audit_rerun_error', array( 'message' => $result->get_error_message(), 'code' => $result->get_error_code(), ) ); } wp_safe_redirect( self::tab_url( 'general', array( 'audited' => 1 ) ) ); exit; } // ---- Per-tab save handlers -------------------------------------------------- /** * Capture an agency/referral code from a settings-page deep link, e.g. an * agency shares * options-general.php?page=agentic-commerce-for-woocommerce&xpay_partner=acme * — the merchant clicks it and is attributed with zero typing. No-op when an * installer pinned the code via the XPAY_WC_PARTNER_CODE constant, or once * connected (attribution is fixed at connect). The value is a non-privileged * attribution token, so nonce-less GET capture is intentional and safe. */ public function maybe_capture_partner_from_query() { if ( Xpay_Partner::is_locked() || Xpay_Plugin::is_connected() ) { return; } if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['xpay_partner'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $code = Xpay_Partner::sanitize( sanitize_text_field( wp_unslash( $_GET['xpay_partner'] ) ) ); if ( '' !== $code ) { Xpay_Partner::save_code( $code ); } } public function handle_save_partner() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_save_partner' ); $code = isset( $_POST['xpay_wc_partner_code'] ) ? sanitize_text_field( wp_unslash( $_POST['xpay_wc_partner_code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing Xpay_Partner::save_code( $code ); wp_safe_redirect( admin_url( 'options-general.php?page=agentic-commerce-for-woocommerce&partner_saved=1' ) ); exit; } public function handle_save_capabilities() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_save_capabilities' ); // Checkbox UI: unchecked boxes don't submit. Default behaviour with no // option set is enabled, so persist explicit 0/1 for every cap. foreach ( self::CAPABILITIES as $cap ) { $on = isset( $_POST[ 'cap_' . $cap ] ) ? 1 : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing update_option( 'xpay_wc_capability_' . $cap, $on ); } wp_safe_redirect( self::tab_url( 'capabilities', array( 'saved' => 1 ) ) ); exit; } public function handle_save_payments() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_save_payments' ); $gateways = $this->available_gateways(); $map = array(); foreach ( $gateways as $gw_id => $_gw ) { $enabled = isset( $_POST[ 'pay_' . $gw_id ] ) ? 1 : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing $label = isset( $_POST[ 'pay_label_' . $gw_id ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'pay_label_' . $gw_id ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( $enabled ) { $map[ $gw_id ] = array( 'enabled' => 1, 'label' => $label, ); } } update_option( 'xpay_wc_payment_map', $map ); wp_safe_redirect( self::tab_url( 'payments', array( 'saved' => 1 ) ) ); exit; } public function handle_save_links() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_save_links' ); $links = array(); foreach ( array_keys( self::LINK_TYPES ) as $type ) { $key = 'link_' . $type; if ( ! isset( $_POST[ $key ] ) ) { continue; } // Sanitize at the boundary (PCP rule WordPress.Security.ValidatedSanitizedInput.InputNotSanitized), // then validate as a URL with esc_url_raw(). $raw = sanitize_text_field( wp_unslash( $_POST[ $key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- check_admin_referer above. $url = esc_url_raw( $raw ); if ( $url ) { $links[ $type ] = $url; } } update_option( 'xpay_wc_links', $links ); wp_safe_redirect( self::tab_url( 'links', array( 'saved' => 1 ) ) ); exit; } public function handle_telemetry_debug_toggle() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_telemetry_debug' ); $current = (bool) get_option( 'xpay_wc_telemetry_debug', 0 ); update_option( 'xpay_wc_telemetry_debug', $current ? 0 : 1 ); wp_safe_redirect( self::tab_url( 'tools', array( 'saved' => 1 ) ) ); exit; } public function handle_refresh_catalog() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_refresh_catalog' ); $slug = Xpay_Plugin::merchant_slug(); $status = 'noop'; if ( $slug ) { $result = Xpay_Client::post( '/v1/merchants/' . rawurlencode( $slug ) . '/resync', array( 'reason' => 'merchant_tools_tab' ) ); $status = is_wp_error( $result ) ? 'error' : 'queued'; } wp_safe_redirect( self::tab_url( 'tools', array( 'resync' => $status ) ) ); exit; } public function handle_test_connection() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_test_connection' ); $slug = Xpay_Plugin::merchant_slug(); $status = 'no_slug'; if ( $slug ) { $result = Xpay_Client::get( '/v1/merchants/' . rawurlencode( $slug ) ); if ( is_wp_error( $result ) ) { $status = 'error'; } else { $status = 'ok'; } } wp_safe_redirect( self::tab_url( 'tools', array( 'tested' => $status ) ) ); exit; } /** * Self-diagnostics. Loopback-probes the three things that silently break a * merchant connection at the web-server layer before WordPress ever runs: * Pretty Permalinks (the WP REST API), our own REST namespace, and the * /.well-known/ discovery files (Apache/ACME shadowing on shared hosting). * Network-only on this explicit click — never on settings render. */ public function handle_run_diagnostics() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'agentic-commerce-for-woocommerce' ) ); } check_admin_referer( 'xpay_wc_run_diagnostics' ); $checks = array( // rest_url() — never hardcode /wp-json (configurable + absent on // Plain permalinks). A non-2xx here means the WP REST API is // unreachable, which blocks the entire connect handshake. 'rest_api' => $this->probe_loopback( rest_url() ), // Our namespace index. 2xx ⇒ the plugin's routes are registered // and reachable; the finalize callback can land. 'xpay_routes' => $this->probe_loopback( rest_url( 'xpay/v1' ) ), // Discovery file at the literal web-server path. A 404 here while // rest_api passes is the classic Apache-reserves-/.well-known case. 'well_known_ucp' => $this->probe_loopback( home_url( '/.well-known/ucp' ) ), ); set_transient( 'xpay_wc_diagnostics', array( 'checks' => $checks, 'at' => time() ), 10 * MINUTE_IN_SECONDS ); wp_safe_redirect( self::tab_url( 'tools', array( 'diagnosed' => 1 ) ) ); exit; } /** * Single loopback GET. Returns ['ok'=>bool, 'code'=>int] (code 0 on a * transport error). Mirrors Xpay_Emitter_Probe's self-fetch posture: * short timeout, follow a couple redirects, don't fail on a self-signed * cert (common on staging). */ private function probe_loopback( $url ) { $res = wp_remote_get( $url, array( 'timeout' => 6, 'redirection' => 2, 'sslverify' => false, 'headers' => array( 'User-Agent' => 'agentic-commerce-for-woocommerce/' . XPAY_WC_VERSION . ' (self-diagnostics)', 'Accept' => 'application/json', ), ) ); if ( is_wp_error( $res ) ) { return array( 'ok' => false, 'code' => 0, ); } $code = (int) wp_remote_retrieve_response_code( $res ); return array( 'ok' => ( $code >= 200 && $code < 300 ), 'code' => $code, ); } // ---- Render ---------------------------------------------------------------- public function render_page() { $connected = Xpay_Plugin::is_connected(); $slug = Xpay_Plugin::merchant_slug(); $last_sync = (int) get_option( 'xpay_wc_last_sync_at', 0 ); $audit = get_option( 'xpay_wc_last_audit' ); // Intentionally no telemetry on settings-page render. The // `Xpay_Telemetry::is_enabled()` gate would short-circuit when the // merchant has not opted in, but emitting any outbound HTTP from a // settings render path is a perception risk under the WP.org // "no phoning home" guideline. Lifecycle events fire from explicit // admin-post handlers only (Connect, Disconnect, telemetry toggle). echo '
' . esc_html__( 'Store disconnected from xpay.', 'agentic-commerce-for-woocommerce' ) . '
' . esc_html__( 'Audit re-run queued. Refresh in ~30 seconds.', 'agentic-commerce-for-woocommerce' ) . '
' . esc_html__( 'Settings saved.', 'agentic-commerce-for-woocommerce' ) . '
' . esc_html( $msg ) . '
' . esc_html( $msg ) . '
';
echo wp_kses_post( __( 'Heads up: a physical robots.txt file is overriding WordPress\'s dynamic robots.txt. xpay\'s shopping-agent allowlist will not apply. Delete the physical file or edit it to allow GPTBot, ClaudeBot, PerplexityBot, and OAI-SearchBot.', 'agentic-commerce-for-woocommerce' ) );
echo '
' . esc_html__( 'Make your products discoverable to AI shoppers. Connecting lets ChatGPT, Claude, Gemini and Perplexity find your catalog and send buyers to your existing checkout — no code, and no change to how you get paid.', 'agentic-commerce-for-woocommerce' ) . '
'; echo '' . esc_html__( 'Safe to try: the plugin never writes files to your site and doesn\'t touch your theme or payments. If you already have an /llms.txt, we add to it rather than replace it. Deactivate anytime and your site is exactly as it was.', 'agentic-commerce-for-woocommerce' ) . '
'; // ⛔ Do not promise screenshots here. The linked page has none, and a link // that over-describes its destination is why merchants arrived expecting a // walkthrough and left. Describe only what is actually on the other side. echo '' . esc_html__( 'What happens when you connect →', 'agentic-commerce-for-woocommerce' ) . '
'; echo '' . esc_html__( 'Connect store →', 'agentic-commerce-for-woocommerce' ) . '
'; echo '' . esc_html__( 'No payment processor change. Payouts continue through your existing WooCommerce gateway.', 'agentic-commerce-for-woocommerce' ) . '
'; echo '' . esc_html__( 'Technical details: publishes /llms.txt, schema.org JSON-LD and an AI-bot robots.txt allowlist, and exposes ACP / UCP / AP2 / MCP endpoints on xpay infrastructure.', 'agentic-commerce-for-woocommerce' ) . '
'; echo '' . esc_html__( 'Were you set up by an agency or partner? Enter their referral code so your store is credited to them. Leave blank if you came on your own.', 'agentic-commerce-for-woocommerce' ) . '
'; if ( $locked ) { echo '' . esc_html( $code ) . ' ' . esc_html__( '(set by your installer in wp-config.php)', 'agentic-commerce-for-woocommerce' ) . '
| ' . esc_html__( 'Merchant slug', 'agentic-commerce-for-woocommerce' ) . ' | ' . esc_html( $slug ) . ' |
|---|---|
| ' . esc_html__( 'Catalog feed', 'agentic-commerce-for-woocommerce' ) . ' | agent-feed.xpay.sh/catalog/' . esc_html( $slug ) . '.json |
| ' . esc_html__( 'Last sync', 'agentic-commerce-for-woocommerce' ) . ' | ' . ( $last_sync ? esc_html( human_time_diff( $last_sync ) . ' ' . __( 'ago', 'agentic-commerce-for-woocommerce' ) ) : esc_html__( 'pending', 'agentic-commerce-for-woocommerce' ) ) . ' |
| ' . esc_html__( 'Last audit score', 'agentic-commerce-for-woocommerce' ) . ' | ' . (int) $audit['score'] . '/100 |
'; echo '' . esc_html__( 'Disconnect', 'agentic-commerce-for-woocommerce' ) . ''; echo '
'; echo 'agent-feed.xpay.sh — ' . esc_html__( 'public CDN that hosts your agent-readable catalog feed (product titles, prices, links, images). No customer or order data.', 'agentic-commerce-for-woocommerce' ) . 'agent-commerce.xpay.sh — ' . esc_html__( 'hosts the ACP / UCP / AP2 / MCP commerce endpoints agents use to build carts and hand off to your checkout.', 'agentic-commerce-for-woocommerce' ) . 'app.xpay.sh — ' . esc_html__( 'your xpay dashboard and the connect flow, opened in a new tab. Not embedded in wp-admin.', 'agentic-commerce-for-woocommerce' ) . 'agent-commerce.xpay.sh/v1/agent-analytics — ' . esc_html__( 'optional, opt-in only: anonymous AI-bot crawl counts (which AI crawler fetched which kind of page, the HTTP status, and whether we routed it to your catalog), plus an aggregate daily count of human pageviews (a number only — no UA, no URL, no per-visit data) so you can see the AI-vs-human split. No customer or order data, no IPs. Batched in the background; shares the Privacy opt-in below.', 'agentic-commerce-for-woocommerce' ) . '' . sprintf( /* translators: 1: terms-of-use link, 2: privacy-policy link */ esc_html__( 'Terms of use: %1$s · Privacy policy: %2$s', 'agentic-commerce-for-woocommerce' ), 'xpay.sh/legal/terms-of-use', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'xpay.sh/legal/privacy-policy' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ) . '
'; } // ---- Tab: Capabilities ---------------------------------------------------- private function render_tab_capabilities() { echo '' . esc_html__( 'Toggle which shopping capabilities are advertised in /.well-known/ucp. All on by default. Switching one off removes it from the manifest agents fetch.', 'agentic-commerce-for-woocommerce' ) . '
'; echo ''; echo '' . esc_html__( 'Map your enabled WooCommerce payment gateways to UCP payment_handlers[]. Agents use this list to decide which payment methods they can complete on the buyer\'s behalf.', 'agentic-commerce-for-woocommerce' ) . '
'; $gateways = $this->available_gateways(); if ( empty( $gateways ) ) { echo '' . esc_html__( 'No payment gateways enabled in WooCommerce yet. Add a gateway under WooCommerce → Settings → Payments first.', 'agentic-commerce-for-woocommerce' ) . '
'; echo '' . esc_html__( 'These appear in the ucp.links array so agents can surface your privacy policy, TOS, contact, etc. to the buyer. Auto-detected where possible; override any field below.', 'agentic-commerce-for-woocommerce' ) . '
'; $saved = get_option( 'xpay_wc_links', array() ); $saved = is_array( $saved ) ? $saved : array(); $detected = $this->detect_links(); $labels = array( 'privacy' => __( 'Privacy policy', 'agentic-commerce-for-woocommerce' ), 'terms' => __( 'Terms of service', 'agentic-commerce-for-woocommerce' ), 'about' => __( 'About', 'agentic-commerce-for-woocommerce' ), 'contact' => __( 'Contact', 'agentic-commerce-for-woocommerce' ), 'shipping' => __( 'Shipping policy', 'agentic-commerce-for-woocommerce' ), ); echo ''; echo ''; echo '' . esc_html__( 'View UCP profile', 'agentic-commerce-for-woocommerce' ) . ' '; if ( $audit_url ) { echo '' . esc_html__( 'View full agent-readiness audit →', 'agentic-commerce-for-woocommerce' ) . ''; } echo '
'; echo '' . sprintf( /* translators: %s: enabled|disabled */ esc_html__( 'Outbound HTTP request logging is %s. When enabled, every xpay backend call is logged to PHP error_log for troubleshooting. Separate from the anonymous-telemetry opt-in on the General tab.', 'agentic-commerce-for-woocommerce' ), '' . esc_html( $state_label ) . '' ) . '
'; echo ''; echo '' . esc_html__( 'Run site diagnostics to confirm AI agents and the xpay backend can reach your store: WordPress Permalinks, the REST API, our plugin routes, and the /.well-known/ discovery files.', 'agentic-commerce-for-woocommerce' ) . '
'; return; } $checks = $diag['checks']; $rest = isset( $checks['rest_api'] ) && is_array( $checks['rest_api'] ) ? $checks['rest_api'] : array( 'ok' => false, 'code' => 0 ); $routes = isset( $checks['xpay_routes'] ) && is_array( $checks['xpay_routes'] ) ? $checks['xpay_routes'] : array( 'ok' => false, 'code' => 0 ); $wk = isset( $checks['well_known_ucp'] ) && is_array( $checks['well_known_ucp'] ) ? $checks['well_known_ucp'] : array( 'ok' => false, 'code' => 0 ); echo '' . esc_html__( 'Your WordPress REST API is not reachable. Go to Settings → Permalinks, choose "Post name", and click Save Changes. This is required before connecting to xpay and for AI agents to read your catalog.', 'agentic-commerce-for-woocommerce' ) . '
' . esc_html__( "The plugin's REST routes are not responding even though the REST API is up. Try deactivating and reactivating the plugin. If it persists, a security plugin (Wordfence, iThemes Security) or a server firewall may be blocking /wp-json/xpay/.", 'agentic-commerce-for-woocommerce' ) . '
';
echo esc_html__( 'Your store is connectable, but /.well-known/ files return an error at the web-server layer. Some hosts (Apache/ACME) reserve /.well-known/ and answer before WordPress runs. AI agents can still reach your discovery profile via this fallback URL:', 'agentic-commerce-for-woocommerce' );
echo ' ' . esc_html( $fallback_url ) . '. ';
echo esc_html__( 'For clean /.well-known/ URLs, ask your host to let WordPress handle /.well-known/ requests.', 'agentic-commerce-for-woocommerce' );
echo '
' . esc_html__( 'All checks passed — agents and xpay can reach your store.', 'agentic-commerce-for-woocommerce' ) . '
'; printf( /* translators: %s: human-readable elapsed time */ esc_html__( 'About %s ago you tried to connect this store to xpay, but the handshake didn\'t finish. Our team has already been notified; in the meantime please click Connect store again — the link is single-use and short-lived for security, so a fresh click is the fastest path to finishing the setup.', 'agentic-commerce-for-woocommerce' ), esc_html( $ago ) ); echo '
'; echo '' . sprintf( /* translators: %s: on|off */ esc_html__( 'Anonymous lifecycle telemetry is %s. No customer data, no order data, no PII is ever sent. We only see plugin lifecycle events (activate, connect, sync errors) tied to your site URL.', 'agentic-commerce-for-woocommerce' ), '' . esc_html( $state_label ) . '' ) . '
'; echo '' . esc_html__( 'This same switch also controls anonymous AI-bot crawl analytics: when on, the plugin counts how AI crawlers (GPTBot, ClaudeBot, PerplexityBot and similar) discover your store — which pages they fetch and whether they reach your discovery files — so you can see this on your xpay dashboard. It records known AI bots plus an aggregate daily count of human pageviews (a number only — no per-visit data) as the AI-vs-human denominator, and never customer, order, or personal data. To disable just this while keeping lifecycle telemetry, add', 'agentic-commerce-for-woocommerce' ) . ' define( \'XPAY_WC_AGENT_ANALYTICS\', false ); ' . esc_html__( 'to wp-config.php.', 'agentic-commerce-for-woocommerce' ) . '
' . esc_html( $toggle_label ) . '
'; } /** * Mirrors the eight checks from scripts/seller-audit/audit.py so the merchant * can see, in their own dashboard, which audit boxes the plugin is satisfying. */ private function render_readiness_checklist() { $slug = Xpay_Plugin::merchant_slug(); $physical_robots = Xpay_Robots::physical_robots_exists(); $rows = array( array( __( 'AI can read your full catalogue', 'agentic-commerce-for-woocommerce' ), (bool) $slug, $slug /* translators: %s: merchant slug */ ? sprintf( __( 'Hosted feed live at agent-feed.xpay.sh/catalog/%s.json', 'agentic-commerce-for-woocommerce' ), $slug ) : __( 'Connect store to enable.', 'agentic-commerce-for-woocommerce' ), ), array( __( 'Live prices visible to AI', 'agentic-commerce-for-woocommerce' ), true, __( 'JSON-LD Product / Offer schema injected on product pages.', 'agentic-commerce-for-woocommerce' ), ), array( __( 'Plain-text guide for AI assistants', 'agentic-commerce-for-woocommerce' ), true, __( 'Served at /llms.txt.', 'agentic-commerce-for-woocommerce' ), ), array( __( 'AI assistants know where to send a buyer', 'agentic-commerce-for-woocommerce' ), (bool) $slug, $slug ? __( 'Per-protocol endpoints (ACP / UCP / AP2 / MCP) advertised in /llms.txt and hosted on xpay infra.', 'agentic-commerce-for-woocommerce' ) : __( 'Connect store to populate endpoints.', 'agentic-commerce-for-woocommerce' ), ), array( __( 'AI shoppers are allowed in', 'agentic-commerce-for-woocommerce' ), ! $physical_robots, $physical_robots ? __( 'Physical robots.txt detected — needs manual fix.', 'agentic-commerce-for-woocommerce' ) : __( 'GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot allowed in robots.txt.', 'agentic-commerce-for-woocommerce' ), ), array( __( 'Direct buy link signals', 'agentic-commerce-for-woocommerce' ), true, __( 'BuyAction emitted on every product page.', 'agentic-commerce-for-woocommerce' ), ), array( __( 'AI shoppers can buy without leaving the chat', 'agentic-commerce-for-woocommerce' ), (bool) $slug, $slug ? __( 'Cart deeplink handler active.', 'agentic-commerce-for-woocommerce' ) : __( 'Connect store to enable.', 'agentic-commerce-for-woocommerce' ), ), array( __( 'Stock & price kept current', 'agentic-commerce-for-woocommerce' ), (bool) $slug, $slug ? __( 'Webhook resync on product/stock change.', 'agentic-commerce-for-woocommerce' ) : __( 'Connect store to enable.', 'agentic-commerce-for-woocommerce' ), ), ); echo '| ' . esc_html__( 'Check', 'agentic-commerce-for-woocommerce' ) . ' | '; echo '' . esc_html__( 'Status', 'agentic-commerce-for-woocommerce' ) . ' | '; echo '' . esc_html__( 'Detail', 'agentic-commerce-for-woocommerce' ) . ' | '; echo '
|---|---|---|
| ' . esc_html( $label ) . ' | '; echo '' . wp_kses_post( $badge ) . ' | '; echo '' . esc_html( $detail ) . ' | '; echo '