PressNative options page (Jetpack-style connect entry).
*
* @return void
*/
public static function add_settings_subpage() {
add_options_page(
__( 'PressNative', 'pressnative-apps' ),
__( 'PressNative', 'pressnative-apps' ),
'manage_options',
'pressnative-settings',
array( __CLASS__, 'render_pressnative_settings_page' )
);
}
/**
* Handles Connect: registers this site with PressNative Cloud server-to-server.
*
* The registry proves wp-admin ownership by calling back to /verify-ownership with a
* single-use challenge, then returns the API key in the response body. The key never
* travels through a browser redirect. New sites get the free plan.
*
* @return void
*/
public static function handle_connect() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'pressnative-apps' ) );
}
check_admin_referer( 'pressnative_connect' );
$hub_opt_in = ! empty( $_POST['pressnative_hub_opt_in'] );
$email = isset( $_POST['pressnative_account_email'] ) ? sanitize_email( wp_unslash( $_POST['pressnative_account_email'] ) ) : '';
$result = self::register_with_registry( $hub_opt_in, $email );
if ( is_wp_error( $result ) ) {
set_transient( 'pressnative_connect_error', $result->get_error_message(), 5 * MINUTE_IN_SECONDS );
}
wp_safe_redirect( admin_url( 'options-general.php?page=pressnative-settings' ) );
exit;
}
/**
* Calls POST /api/v1/sites/register and stores the returned API key.
*
* @param bool $hub_opt_in Whether to list the site in the PressNative Hub.
* @param string $email Optional account email for service notices.
* @return true|WP_Error
*/
public static function register_with_registry( $hub_opt_in, $email = '' ) {
$challenge = wp_generate_password( 48, false );
set_transient( 'pressnative_verify_nonce', $challenge, 5 * MINUTE_IN_SECONDS );
$response = wp_remote_post(
rtrim( self::get_registry_url(), '/' ) . '/api/v1/sites/register',
array(
'timeout' => 20,
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => wp_json_encode(
array(
'site_url' => home_url(),
'verify_url' => rest_url( 'pressnative/v1/verify-ownership' ),
'challenge' => $challenge,
'hub_opt_in' => (bool) $hub_opt_in,
'tags' => PressNative_Options::get_app_categories(),
'site_name' => get_bloginfo( 'name' ),
'admin_email' => $email,
'wp_version' => get_bloginfo( 'version' ),
'plugin_version' => PRESSNATIVE_VERSION,
)
),
)
);
delete_transient( 'pressnative_verify_nonce' );
if ( is_wp_error( $response ) ) {
return new WP_Error(
'pressnative_connect_unreachable',
/* translators: %s: error message */
sprintf( __( 'Could not reach PressNative Cloud: %s', 'pressnative-apps' ), $response->get_error_message() )
);
}
$code = (int) wp_remote_retrieve_response_code( $response );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( $code < 200 || $code >= 300 || empty( $data['api_key'] ) || ! is_string( $data['api_key'] ) ) {
$message = is_array( $data ) && ! empty( $data['error'] ) ? (string) $data['error'] : sprintf( 'HTTP %d', $code );
if ( is_array( $data ) && ! empty( $data['hint'] ) ) {
$message .= ' ' . (string) $data['hint'];
}
/* translators: %s: error returned by PressNative Cloud */
return new WP_Error( 'pressnative_connect_failed', sprintf( __( 'Connect failed: %s', 'pressnative-apps' ), $message ) );
}
// Saved before the key so this change does not trigger a sync of its own.
update_option( PressNative_Options::OPTION_HUB_OPTED_IN, $hub_opt_in ? '1' : '0' );
// Registration already proved ownership; skip the separate /api/verify-site round trip.
remove_action( 'update_option_' . self::OPTION_API_KEY, array( __CLASS__, 'trigger_site_verification' ), 10 );
remove_action( 'add_option_' . self::OPTION_API_KEY, array( __CLASS__, 'trigger_site_verification_on_add' ), 10 );
$api_key = sanitize_text_field( $data['api_key'] );
update_option( self::OPTION_API_KEY, $api_key );
update_option( 'pressnative_site_verified', current_time( 'mysql' ) );
delete_transient( 'pressnative_connect_error' );
PressNative_Registry_Notify::sync_now( $api_key );
PressNative_AOT_Compiler::run_initial_sweep( 10 );
return true;
}
/**
* Handles Disconnect: clears API key and redirects to Settings > PressNative.
*
* @return void
*/
public static function handle_disconnect() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'pressnative-apps' ) );
}
check_admin_referer( 'pressnative_disconnect' );
delete_option( self::OPTION_API_KEY );
wp_safe_redirect( admin_url( 'options-general.php?page=pressnative-settings' ) );
exit;
}
/**
* Renders the Settings > PressNative page: Connect hero or Connected state with masked key and Disconnect.
*
* @return void
*/
public static function render_pressnative_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$api_key = get_option( self::OPTION_API_KEY, '' );
$connect_error = get_transient( 'pressnative_connect_error' );
?>
8 ? substr( $api_key, 0, 8 ) . '****' . substr( $api_key, -4 ) : '****' . substr( $api_key, -4 );
?>
$item ) {
if ( ! in_array( $slug, $order, true ) ) {
$reordered[] = $item;
}
}
$submenu['pressnative'] = $reordered;
$submenu['pressnative'][0][0] = __( 'Dashboard', 'pressnative-apps' );
}
/**
* Registers Registry URL setting.
*
* @return void
*/
public static function register_settings() {
register_setting(
'pressnative_settings',
self::OPTION_REGISTRY_URL,
array(
'type' => 'string',
'sanitize_callback' => array( __CLASS__, 'sanitize_registry_url' ),
'default' => self::DEFAULT_REGISTRY_URL,
)
);
register_setting(
'pressnative_settings',
self::OPTION_API_KEY,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
// App Settings (branding)
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_APP_NAME,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => PressNative_Options::DEFAULT_APP_NAME,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_PRIMARY_COLOR,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_primary_color' ),
'default' => PressNative_Options::DEFAULT_PRIMARY_COLOR,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_ACCENT_COLOR,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_hex' ),
'default' => PressNative_Options::DEFAULT_ACCENT_COLOR,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_LOGO_ATTACHMENT,
array(
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 0,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_BACKGROUND_COLOR,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_hex' ),
'default' => PressNative_Options::DEFAULT_BACKGROUND_COLOR,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_BACKGROUND_IMAGE,
array(
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 0,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_TILE_BACKGROUND_COLOR,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_hex' ),
'default' => PressNative_Options::DEFAULT_TILE_BACKGROUND_COLOR,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_TILE_BACKGROUND,
array(
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 0,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_TEXT_COLOR,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_text_color' ),
'default' => PressNative_Options::DEFAULT_TEXT_COLOR,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_BORDER_COLOR,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_hex' ),
'default' => PressNative_Options::DEFAULT_BORDER_COLOR,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_TILE_TEXT_COLOR,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_hex' ),
'default' => PressNative_Options::DEFAULT_TILE_TEXT_COLOR,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_FONT_FAMILY,
array(
'type' => 'string',
'sanitize_callback' => function ( $v ) {
$allowed = array( 'sans-serif', 'serif', 'monospace' );
return in_array( $v, $allowed, true ) ? $v : PressNative_Options::DEFAULT_FONT_FAMILY;
},
'default' => PressNative_Options::DEFAULT_FONT_FAMILY,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_BASE_FONT_SIZE,
array(
'type' => 'integer',
'sanitize_callback' => function ( $v ) {
$v = absint( $v );
return max( 12, min( 24, $v ) );
},
'default' => PressNative_Options::DEFAULT_BASE_FONT_SIZE,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_APP_CATEGORIES,
array(
'type' => 'array',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_app_categories' ),
'default' => PressNative_Options::DEFAULT_APP_CATEGORIES,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_NOTIFICATION_PREFERENCES,
array(
'type' => 'array',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_notification_preferences' ),
'default' => PressNative_Options::DEFAULT_NOTIFICATION_PREFERENCES,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_HUB_OPTED_IN,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_hub_opted_in' ),
'default' => PressNative_Options::DEFAULT_HUB_OPTED_IN ? '1' : '0',
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_SHORT_DESCRIPTION,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_FULL_DESCRIPTION,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_textarea_field',
'default' => '',
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_KEYWORDS,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_store_keywords' ),
'default' => '',
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_CATEGORY,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => PressNative_Options::DEFAULT_STORE_CATEGORY,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_PRIVACY_POLICY_URL,
array(
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
'default' => '',
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_SUPPORT_URL,
array(
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
'default' => '',
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_MARKETING_URL,
array(
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
'default' => '',
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_COPYRIGHT,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_ICON_ATTACHMENT_ID,
array(
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 0,
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_STORE_CONTENT_RATING,
array(
'type' => 'string',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_content_rating' ),
'default' => wp_json_encode( PressNative_Options::DEFAULT_CONTENT_RATING ),
)
);
register_setting(
'pressnative_app_settings',
PressNative_Options::OPTION_APP_LINK_SETTINGS,
array(
'type' => 'array',
'sanitize_callback' => array( 'PressNative_Options', 'sanitize_app_link_settings' ),
'default' => PressNative_Options::DEFAULT_APP_LINK_SETTINGS,
)
);
register_setting(
'pressnative_commerce_settings',
PressNative_Cart_Recovery::OPTION_ENABLED,
array(
'type' => 'string',
'sanitize_callback' => function ( $v ) {
return '0' === (string) $v ? '0' : '1';
},
'default' => '1',
)
);
register_setting(
'pressnative_commerce_settings',
PressNative_Cart_Recovery::OPTION_DELAY_MINUTES,
array(
'type' => 'integer',
'sanitize_callback' => function ( $v ) {
return max( 15, min( 24 * 60, absint( $v ) ) );
},
'default' => 60,
)
);
register_setting(
'pressnative_commerce_settings',
PressNative_Cart_Recovery::OPTION_BACK_IN_STOCK_ENABLED,
array(
'type' => 'string',
'sanitize_callback' => function ( $v ) {
return '0' === (string) $v ? '0' : '1';
},
'default' => '1',
)
);
// Product Display Settings (WooCommerce)
register_setting(
'pressnative_app_settings',
'pressnative_product_in_post_style',
array(
'type' => 'string',
'sanitize_callback' => function ( $v ) {
$allowed = array( 'compact_row', 'card', 'mini_card' );
return in_array( $v, $allowed, true ) ? $v : 'compact_row';
},
'default' => 'compact_row',
)
);
register_setting(
'pressnative_app_settings',
'pressnative_product_grid_style',
array(
'type' => 'string',
'sanitize_callback' => function ( $v ) {
$allowed = array( 'compact_row', 'card', 'mini_card' );
return in_array( $v, $allowed, true ) ? $v : 'card';
},
'default' => 'card',
)
);
// Layout Settings
register_setting(
'pressnative_layout_settings',
PressNative_Layout_Options::OPTION_HERO_CATEGORY_SLUG,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => PressNative_Layout_Options::DEFAULT_HERO_CATEGORY_SLUG,
)
);
register_setting(
'pressnative_layout_settings',
PressNative_Layout_Options::OPTION_HERO_MAX_ITEMS,
array(
'type' => 'integer',
'sanitize_callback' => function ( $v ) {
$v = absint( $v );
return max( 1, min( 10, $v ) );
},
'default' => PressNative_Layout_Options::DEFAULT_HERO_MAX_ITEMS,
)
);
register_setting(
'pressnative_layout_settings',
PressNative_Layout_Options::OPTION_POST_GRID_COLUMNS,
array(
'type' => 'integer',
'sanitize_callback' => function ( $v ) {
$v = absint( $v );
return max( 1, min( 4, $v ) );
},
'default' => PressNative_Layout_Options::DEFAULT_POST_GRID_COLUMNS,
)
);
register_setting(
'pressnative_layout_settings',
PressNative_Layout_Options::OPTION_POST_GRID_PER_PAGE,
array(
'type' => 'integer',
'sanitize_callback' => function ( $v ) {
$v = absint( $v );
return max( 1, min( 50, $v ) );
},
'default' => PressNative_Layout_Options::DEFAULT_POST_GRID_PER_PAGE,
)
);
register_setting(
'pressnative_layout_settings',
PressNative_Layout_Options::OPTION_ENABLED_CATEGORIES,
array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'sanitize_category_ids' ),
'default' => array(),
)
);
register_setting(
'pressnative_layout_settings',
PressNative_Layout_Options::OPTION_ENABLED_COMPONENTS,
array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'sanitize_component_ids' ),
'default' => PressNative_Layout_Options::COMPONENT_IDS,
)
);
register_setting(
'pressnative_layout_settings',
PressNative_Layout_Options::OPTION_SPONSOR_ARTICLE_INTERVAL,
array(
'type' => 'integer',
'sanitize_callback' => function ( $v ) {
$v = absint( $v );
return max( 0, min( 50, $v ) );
},
'default' => PressNative_Layout_Options::DEFAULT_SPONSOR_ARTICLE_INTERVAL,
)
);
}
/**
* Sanitizes category IDs array.
*
* @param mixed $value Raw value.
* @return int[]
*/
public static function sanitize_category_ids( $value ) {
if ( empty( $value ) || ! is_array( $value ) ) {
return array();
}
$ids = array_map( 'absint', $value );
$ids = array_filter( $ids );
return array_values( array_unique( $ids ) );
}
/**
* Sanitizes component IDs (comma-separated string or array).
*
* @param mixed $value Raw value (string "id1,id2" or array).
* @return string[]
*/
public static function sanitize_component_ids( $value ) {
$ids = array();
if ( is_string( $value ) && '' !== trim( $value ) ) {
$ids = array_map( 'trim', explode( ',', $value ) );
} elseif ( is_array( $value ) ) {
$ids = array_map( 'sanitize_text_field', $value );
}
$valid = array_values( array_intersect( array_filter( $ids ), PressNative_Layout_Options::COMPONENT_IDS ) );
return empty( $valid ) ? PressNative_Layout_Options::COMPONENT_IDS : $valid;
}
/**
* Sanitizes Registry URL (ensure no trailing slash, valid URL).
*
* @param string $value Raw input.
* @return string
*/
public static function sanitize_registry_url( $value ) {
$value = trim( (string) $value );
if ( '' === $value ) {
return self::DEFAULT_REGISTRY_URL;
}
$value = rtrim( $value, '/' );
if ( ! preg_match( '#^https?://#', $value ) ) {
return 'http://' . $value;
}
return $value;
}
const SUBSCRIPTION_CACHE_KEY = 'pressnative_subscription_status';
const SUBSCRIPTION_CACHE_TTL = 300;
/**
* Fetches the subscription status from the Registry via Stripe API.
* Cached for 5 minutes to avoid Registry calls on every admin page load.
*
* @return array|null Subscription data or null on failure.
*/
/**
* Whether the connected site's plan includes paid features (ad-hoc push, analytics).
* Unknown (registry unreachable or older registry) counts as yes so nothing is hidden.
*
* @return bool
*/
public static function has_paid_features() {
$status = self::fetch_subscription_status();
if ( ! is_array( $status ) || ! array_key_exists( 'paid_features', $status ) ) {
return true;
}
return (bool) $status['paid_features'];
}
/**
* Inline (non-dismissible, page-scoped) note explaining what the free plan includes.
*
* @param string $feature Human-readable feature name.
* @return void
*/
private static function render_free_plan_note( $feature ) {
if ( '' === (string) get_option( self::OPTION_API_KEY, '' ) || self::has_paid_features() ) {
return;
}
?>
See plans.', 'pressnative-apps' ), array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array() ) ) ),
esc_html( $feature ),
esc_url( 'https://pressnative.app/pricing' )
);
?>
10,
'headers' => array(
'X-PressNative-API-Key' => $api_key,
),
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$code = wp_remote_retrieve_response_code( $response );
if ( $code !== 200 ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( is_array( $data ) ) {
set_transient( $cache_key, $data, self::SUBSCRIPTION_CACHE_TTL );
return $data;
}
return null;
}
/**
* Creates a Stripe Customer Portal session URL.
*
* @return string|null Portal URL or null on failure.
*/
public static function get_stripe_portal_url() {
$api_key = get_option( self::OPTION_API_KEY, '' );
$registry_url = self::get_registry_url();
if ( empty( $api_key ) || empty( $registry_url ) ) {
return null;
}
$url = rtrim( $registry_url, '/' ) . '/api/stripe/portal';
$response = wp_remote_post(
$url,
array(
'timeout' => 10,
'headers' => array(
'Content-Type' => 'application/json',
'X-PressNative-API-Key' => $api_key,
),
'body' => wp_json_encode(
array(
'return_url' => admin_url( 'admin.php?page=pressnative' ),
)
),
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
return isset( $data['url'] ) ? $data['url'] : null;
}
/**
* Renders the PressNative settings page.
*
* @return void
*/
public static function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$registry_url = get_option( self::OPTION_REGISTRY_URL, self::DEFAULT_REGISTRY_URL );
$api_key = get_option( self::OPTION_API_KEY, '' );
$verified = get_option( self::OPTION_SCHEMA_VERIFIED, '' );
$site_verified = get_option( 'pressnative_site_verified', '' );
$sub_status = ! empty( $api_key ) ? self::fetch_subscription_status() : null;
?>
rest_url(),
'nonce' => wp_create_nonce( 'wp_rest' ),
'woocommerceActive' => class_exists( 'PressNative_WooCommerce' ) && PressNative_WooCommerce::is_active(),
)
);
}
if ( $layout_settings ) {
wp_enqueue_script( 'jquery-ui-sortable' );
wp_add_inline_script(
'jquery-ui-sortable',
self::get_component_order_script(),
'after'
);
wp_add_inline_style(
'pressnative-preview',
'.pressnative-sortable-placeholder{height:40px;background:#f0f0f1;border:2px dashed #c3c4c7;margin-bottom:6px;list-style:none;}'
. '.pressnative-sortable-list{list-style:none;margin:0;padding:0;}'
. '.pressnative-sortable-item{margin-bottom:6px;padding:8px;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:4px;}'
. '.pressnative-drag-handle{cursor:move;margin-right:8px;color:#787c82;}'
. '.pressnative-sortable-item label{cursor:pointer;margin:0;}'
);
}
if ( $analytics_page ) {
wp_enqueue_script(
'pressnative-chartjs',
plugins_url( 'assets/js/vendor/chart.umd.min.js', PRESSNATIVE_PLUGIN_DIR . 'pressnative.php' ),
array(),
'4.5.1',
true
);
$analytics_js = PRESSNATIVE_PLUGIN_DIR . 'assets/js/analytics-dashboard.js';
$analytics_css = PRESSNATIVE_PLUGIN_DIR . 'assets/css/analytics-dashboard.css';
if ( file_exists( $analytics_js ) ) {
wp_enqueue_script(
'pressnative-analytics',
plugins_url( 'assets/js/analytics-dashboard.js', PRESSNATIVE_PLUGIN_DIR . 'pressnative.php' ),
array( 'pressnative-chartjs' ),
filemtime( $analytics_js ),
true
);
wp_localize_script(
'pressnative-analytics',
'pressnativeAnalytics',
array(
'restUrl' => rest_url( 'pressnative/v1' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'editPostUrl' => admin_url( 'post.php' ),
)
);
}
if ( file_exists( $analytics_css ) ) {
wp_enqueue_style(
'pressnative-analytics',
plugins_url( 'assets/css/analytics-dashboard.css', PRESSNATIVE_PLUGIN_DIR . 'pressnative.php' ),
array(),
filemtime( $analytics_css )
);
}
}
if ( $app_settings ) {
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_media();
wp_add_inline_script(
'wp-color-picker',
'jQuery(function($){var t;function schedule(){if(t)clearTimeout(t);t=setTimeout(function(){t=null;if(typeof window.pressnativePreviewRefresh==="function")window.pressnativePreviewRefresh();},300);}$(".pressnative-color-picker").wpColorPicker({change:schedule,clear:schedule});});'
);
wp_add_inline_script(
'wp-color-picker',
self::get_logo_upload_script() . "\n" . self::get_media_upload_script() . "\n" . self::get_app_categories_script() . "\n" . self::get_app_links_script(),
'after'
);
}
}
/**
* Inline script for component order sortable and hidden input sync.
*
* @return string
*/
private static function get_component_order_script() {
return "
jQuery(function($){
var \$list = $('#pressnative-component-order');
var \$hidden = $('#pressnative-component-order-value');
function updateOrder() {
var order = [];
\$list.find('li').each(function(){
var cb = $(this).find('input[type=checkbox]');
if (cb.length && cb.prop('checked')) {
order.push(cb.val());
}
});
\$hidden.val(order.join(','));
if (typeof window.pressnativePreviewRefresh === 'function') {
window.pressnativePreviewRefresh();
}
}
\$list.sortable({
handle: '.pressnative-drag-handle',
placeholder: 'pressnative-sortable-placeholder',
items: '> li'
});
\$list.on('sortupdate', updateOrder);
\$list.on('change', 'input[type=checkbox]', updateOrder);
});
";
}
/**
* Inline script for logo upload (WP Media Library).
*
* @return string
*/
private static function get_logo_upload_script() {
return "
jQuery(function($){
var frame;
$('#pressnative_logo_select').on('click', function(e){
e.preventDefault();
if (frame) { frame.open(); return; }
frame = wp.media({
title: 'Select or upload logo',
library: { type: 'image' },
button: { text: 'Use this image' },
multiple: false
});
frame.on('select', function(){
var att = frame.state().get('selection').first().toJSON();
$('#pressnative_logo_attachment_id').val(att.id);
$('#pressnative_logo_preview').html('
');
if (typeof window.pressnativePreviewRefresh === 'function') { window.pressnativePreviewRefresh(); }
});
frame.open();
});
$('#pressnative_logo_remove').on('click', function(e){
e.preventDefault();
$('#pressnative_logo_attachment_id').val('');
$('#pressnative_logo_preview').html('');
if (typeof window.pressnativePreviewRefresh === 'function') { window.pressnativePreviewRefresh(); }
});
});
";
}
/**
* Inline script for generic media upload buttons (background image, tile background).
*
* @return string
*/
private static function get_media_upload_script() {
return "
jQuery(function($){
var frames = {};
$('.pressnative-media-upload').on('click', function(e){
e.preventDefault();
var \$btn = $(this);
var target = \$btn.attr('data-target');
var preview = \$btn.attr('data-preview');
var title = \$btn.attr('data-title') || 'Select or upload image';
var buttonText = \$btn.attr('data-button') || 'Use this image';
if (frames[target]) {
frames[target].open();
return;
}
frames[target] = wp.media({
title: title,
library: { type: 'image' },
button: { text: buttonText },
multiple: false
});
frames[target].on('select', function(){
var att = frames[target].state().get('selection').first().toJSON();
$(target).val(att.id);
if (preview) {
$(preview).attr('src', att.url).show();
\$btn.siblings('.pressnative-media-remove').show();
}
if (typeof window.pressnativePreviewRefresh === 'function') {
window.pressnativePreviewRefresh();
}
});
frames[target].open();
});
$('.pressnative-media-remove').on('click', function(e){
e.preventDefault();
var \$btn = $(this);
var target = \$btn.attr('data-target');
var preview = \$btn.attr('data-preview');
if (target) $(target).val('');
if (preview) $(preview).attr('src', '').hide();
\$btn.hide();
if (typeof window.pressnativePreviewRefresh === 'function') {
window.pressnativePreviewRefresh();
}
});
});
";
}
/**
* Inline script for App Categories tag pills (pill display, add via Enter/comma, remove).
*
* @return string
*/
private static function get_app_categories_script() {
return "
jQuery(function($){
var \$hidden = $('#pressnative_app_categories');
var \$textInput = $('#pressnative_app_categories_input');
var \$wrapper = $('.pressnative-tag-input-wrapper');
var max = parseInt(\$hidden.attr('data-max') || 5, 10);
function getTags() {
var val = (\$hidden.val() || '').trim();
return val ? val.split(',').map(function(t){ return t.trim(); }).filter(Boolean) : [];
}
function setTags(tags) {
tags = tags.slice(0, max);
\$hidden.val(tags.join(','));
\$wrapper.find('.pressnative-tag-pill-display').remove();
tags.forEach(function(tag) {
var safeTag = $('').text(tag).html();
var safeAttr = tag.replace(/\"/g, '"');
var pill = '' +
safeTag +
' ×' +
'';
$(pill).insertBefore(\$textInput);
});
updateSuggestedPills();
}
function updateSuggestedPills() {
var tags = getTags();
$('.pressnative-tag-pill').each(function() {
var tag = String($(this).attr('data-tag') || '');
var active = tags.indexOf(tag) >= 0;
$(this).toggleClass('active', active).css({
'background': active ? '#2271b1' : '',
'color': active ? '#fff' : '',
'border-color': active ? '#2271b1' : '#8c8f94'
});
});
}
$('.pressnative-tag-pill').on('click', function() {
var tag = String($(this).attr('data-tag') || '');
var tags = getTags();
var idx = tags.indexOf(tag);
if (idx >= 0) {
tags.splice(idx, 1);
} else if (tags.length < max) {
tags.push(tag);
}
setTags(tags);
});
\$textInput.on('keydown', function(e) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
var val = ($(this).val() || '').trim();
if (val) {
var tags = getTags();
if (tags.indexOf(val) < 0 && tags.length < max) {
tags.push(val);
setTags(tags);
}
$(this).val('');
}
} else if (e.key === 'Backspace' && !$(this).val()) {
var tags = getTags();
if (tags.length) {
tags.pop();
setTags(tags);
}
}
});
\$textInput.on('blur', function() {
var val = ($(this).val() || '').trim();
if (val) {
var tags = getTags();
if (tags.indexOf(val) < 0 && tags.length < max) {
tags.push(val);
setTags(tags);
}
$(this).val('');
}
});
$(document).on('click', '.pressnative-tag-remove', function(e) {
e.preventDefault();
e.stopPropagation();
var tag = String($(this).attr('data-tag') || '');
if (!tag) return;
var tags = getTags().filter(function(t) { return t !== tag; });
setTags(tags);
});
setTags(getTags());
});
";
}
/**
* Inline script for App Links verify and download buttons.
*
* @return string
*/
private static function get_app_links_script() {
$rest_url = esc_url_raw( rest_url( 'pressnative/v1' ) );
$nonce = wp_create_nonce( 'wp_rest' );
return "
jQuery(function($){
var restUrl = " . wp_json_encode( $rest_url ) . ";
var nonce = " . wp_json_encode( $nonce ) . ";
$('#pressnative-verify-app-links').on('click', function(){
var \$btn = $(this);
var \$status = $('#pressnative-app-links-status');
\$btn.prop('disabled', true);
\$status.text('Verifying...');
$.ajax({
url: restUrl + '/verify-app-links',
method: 'POST',
beforeSend: function(xhr){ xhr.setRequestHeader('X-WP-Nonce', nonce); },
success: function(r){
if (r.ok) {
\$status.html('✓ Association files verified');
} else {
var msgs = [];
if (r.results && r.results.aasa && r.results.aasa.errors) msgs = msgs.concat(r.results.aasa.errors);
if (r.results && r.results.assetlinks && r.results.assetlinks.errors) msgs = msgs.concat(r.results.assetlinks.errors);
\$status.html('' + (msgs.join('; ') || 'Verification failed') + '');
}
},
error: function(xhr){
\$status.html('Error: ' + (xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Request failed') + '');
},
complete: function(){ \$btn.prop('disabled', false); }
});
});
});
";
}
/**
* Renders the App Settings page (branding: app name, colors, logo).
*
* @return void
*/
public static function render_app_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$app_name = get_option( PressNative_Options::OPTION_APP_NAME, PressNative_Options::DEFAULT_APP_NAME );
$primary_color = get_option( PressNative_Options::OPTION_PRIMARY_COLOR, PressNative_Options::DEFAULT_PRIMARY_COLOR );
$accent_color = get_option( PressNative_Options::OPTION_ACCENT_COLOR, PressNative_Options::DEFAULT_ACCENT_COLOR );
$logo_attachment = (int) get_option( PressNative_Options::OPTION_LOGO_ATTACHMENT, 0 );
$logo_preview = '';
if ( $logo_attachment > 0 ) {
$url = wp_get_attachment_image_url( $logo_attachment, 'thumbnail' );
if ( $url ) {
$logo_preview = '
';
}
}
?>
' . esc_html__( 'Launch kit applied. Review colors and layout below, then save if you customize further.', 'pressnative-apps' ) . '
';
}
?>
false, 'parent' => 0 ) );
$component_labels = array(
'nav-menu' => __( 'Nav Menu', 'pressnative-apps' ),
'hero-carousel' => __( 'Hero Carousel', 'pressnative-apps' ),
'post-grid' => __( 'Post Grid', 'pressnative-apps' ),
'category-list' => __( 'Category List', 'pressnative-apps' ),
'page-list' => __( 'Page List', 'pressnative-apps' ),
'block-sponsor' => __( 'Block Sponsor', 'pressnative-apps' ),
'product-grid' => __( 'Product Grid (WooCommerce)', 'pressnative-apps' ),
'product-category-list' => __( 'Product Categories (WooCommerce)', 'pressnative-apps' ),
'product-carousel' => __( 'Product Carousel (WooCommerce)', 'pressnative-apps' ),
);
$sponsor_interval = PressNative_Layout_Options::get_sponsor_article_interval();
?>
get_error_message() ),
admin_url( 'admin.php?page=pressnative-app-settings' )
)
);
exit;
}
wp_safe_redirect( add_query_arg( 'pressnative_kit_applied', '1', admin_url( 'admin.php?page=pressnative-app-settings' ) ) );
exit;
}
/**
* Handles ad-hoc push form submission. POSTs to Registry with API key.
*
* @return void
*/
public static function handle_send_push() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'pressnative-apps' ) );
}
if ( ! isset( $_POST['pressnative_push_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pressnative_push_nonce'] ) ), 'pressnative_send_push' ) ) {
wp_safe_redirect( add_query_arg( 'pressnative_push_error', 'nonce', admin_url( 'admin.php?page=pressnative-push' ) ) );
exit;
}
$api_key = get_option( self::OPTION_API_KEY, '' );
$registry_url = self::get_registry_url();
if ( ! $api_key || ! $registry_url ) {
wp_safe_redirect( add_query_arg( 'pressnative_push_error', 'no_api_key', admin_url( 'admin.php?page=pressnative-push' ) ) );
exit;
}
$title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '';
$body = isset( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : '';
if ( ! $title || ! $body ) {
wp_safe_redirect( add_query_arg( 'pressnative_push_error', 'missing_fields', admin_url( 'admin.php?page=pressnative-push' ) ) );
exit;
}
$link = isset( $_POST['link'] ) ? esc_url_raw( wp_unslash( $_POST['link'] ) ) : '';
$image_url = isset( $_POST['image_url'] ) ? esc_url_raw( wp_unslash( $_POST['image_url'] ) ) : '';
$device_type = isset( $_POST['device_type'] ) ? sanitize_text_field( wp_unslash( $_POST['device_type'] ) ) : 'all';
if ( ! in_array( $device_type, array( 'all', 'ios', 'android' ), true ) ) {
$device_type = 'all';
}
$engaged_since_days = isset( $_POST['engaged_since_days'] ) ? sanitize_text_field( wp_unslash( $_POST['engaged_since_days'] ) ) : '';
if ( in_array( $engaged_since_days, array( '7', '30' ), true ) ) {
$engaged_since_days = (int) $engaged_since_days;
} else {
$engaged_since_days = null;
}
$url = rtrim( $registry_url, '/' ) . '/api/v1/push/send';
$body_data = array(
'title' => $title,
'body' => $body,
'link' => $link,
'image_url' => $image_url,
'device_type' => $device_type,
'engaged_since_days' => $engaged_since_days,
);
$response = wp_remote_post(
$url,
array(
'timeout' => 15,
'blocking' => true,
'headers' => array(
'Content-Type' => 'application/json',
'X-PressNative-API-Key' => $api_key,
),
'body' => wp_json_encode( $body_data ),
)
);
if ( is_wp_error( $response ) ) {
wp_safe_redirect( add_query_arg( 'pressnative_push_error', 'request_failed', admin_url( 'admin.php?page=pressnative-push' ) ) );
exit;
}
$code = wp_remote_retrieve_response_code( $response );
$body_res = wp_remote_retrieve_body( $response );
$data = json_decode( $body_res, true );
if ( $code >= 200 && $code < 300 ) {
$sent = isset( $data['sent'] ) ? (int) $data['sent'] : 0;
wp_safe_redirect( add_query_arg( 'pressnative_push_sent', $sent, admin_url( 'admin.php?page=pressnative-push' ) ) );
exit;
}
if ( ! empty( $data['message'] ) ) {
$err_msg = (string) $data['message'];
} else {
$err_msg = isset( $data['error'] ) ? $data['error'] : __( 'Unknown error', 'pressnative-apps' );
}
wp_safe_redirect( add_query_arg( array( 'pressnative_push_error' => 'registry', 'pressnative_push_message' => rawurlencode( $err_msg ) ), admin_url( 'admin.php?page=pressnative-push' ) ) );
exit;
}
/**
* Renders the Push Notifications page (ad-hoc send).
*
* @return void
*/
public static function render_push_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$api_key = get_option( self::OPTION_API_KEY, '' );
$registry_url = self::get_registry_url();
$has_config = ! empty( $api_key ) && ! empty( $registry_url );
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- display-only result params from redirect after sending push
$error = isset( $_GET['pressnative_push_error'] ) ? sanitize_text_field( wp_unslash( $_GET['pressnative_push_error'] ) ) : '';
$sent = isset( $_GET['pressnative_push_sent'] ) ? (int) $_GET['pressnative_push_sent'] : 0;
$err_msg = isset( $_GET['pressnative_push_message'] ) ? sanitize_text_field( wp_unslash( $_GET['pressnative_push_message'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$stats = $has_config ? self::fetch_push_subscriber_stats() : null;
$stats = is_array( $stats ) ? $stats : array( 'total' => 0, 'ios' => 0, 'android' => 0, 'engaged_7d' => 0, 'engaged_30d' => 0 );
?>
0 ) : ?>
0 || ( $stats['engaged_30d'] ?? 0 ) > 0 ) : ?>
0 ) : ?>
10,
'headers' => array(
'Accept' => 'application/json',
'X-PressNative-API-Key' => $api_key,
),
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! is_array( $data ) ) {
return null;
}
return array(
'total' => isset( $data['total'] ) ? (int) $data['total'] : 0,
'ios' => isset( $data['ios'] ) ? (int) $data['ios'] : 0,
'android' => isset( $data['android'] ) ? (int) $data['android'] : 0,
'engaged_7d' => isset( $data['engaged_7d'] ) ? (int) $data['engaged_7d'] : 0,
'engaged_30d' => isset( $data['engaged_30d'] ) ? (int) $data['engaged_30d'] : 0,
);
}
/**
* Returns the configured Registry URL.
* Production default is https://pressnative.app.
* On localhost, uses the user-configured value (which defaults to localhost:3000 for dev).
*
* @return string
*/
public static function get_registry_url() {
if ( self::is_localhost() ) {
$saved = get_option( self::OPTION_REGISTRY_URL, '' );
return ! empty( $saved ) ? $saved : 'http://localhost:3000';
}
return self::DEFAULT_REGISTRY_URL;
}
/**
* Checks if the current WordPress site is running on localhost.
*
* @return bool
*/
public static function is_localhost() {
$home = home_url();
$parsed = wp_parse_url( $home );
$host = isset( $parsed['host'] ) ? strtolower( $parsed['host'] ) : '';
return in_array( $host, array( 'localhost', '127.0.0.1' ), true );
}
/**
* Handles the Stripe portal redirect when ?pressnative_portal=1 is present.
*
* @return void
*/
public static function handle_stripe_portal_redirect() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- redirect trigger from our settings page; capability checked below
if ( empty( $_GET['pressnative_portal'] ) || '1' !== $_GET['pressnative_portal'] ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Only handle on our settings page.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- page slug check for redirect target
if ( empty( $_GET['page'] ) || 'pressnative' !== $_GET['page'] ) {
return;
}
$portal_url = self::get_stripe_portal_url();
if ( $portal_url ) {
wp_safe_redirect( $portal_url );
exit;
}
// If portal URL failed, redirect back with an error.
wp_safe_redirect( add_query_arg( 'pressnative_portal_error', '1', admin_url( 'admin.php?page=pressnative' ) ) );
exit;
}
/**
* Pings the Registry to verify schema (GET /api/v1/schema). Saves verification timestamp on success.
*
* @return bool True if schema was verified, false otherwise.
*/
public static function verify_registry_schema() {
$base = self::get_registry_url();
$url = rtrim( $base, '/' ) . '/api/v1/schema';
$response = wp_remote_get(
$url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return false;
}
$code = wp_remote_retrieve_response_code( $response );
if ( $code !== 200 ) {
return false;
}
$body = wp_remote_retrieve_body( $response );
$json = json_decode( $body, true );
if ( ! is_array( $json ) || ! isset( $json['branding'] ) || ! isset( $json['screen'] ) || ! isset( $json['components'] ) ) {
return false;
}
update_option( self::OPTION_SCHEMA_VERIFIED, current_time( 'mysql' ) );
return true;
}
/**
* Triggers site verification when the API key is saved/updated.
*
* @param string $old_value Previous API key.
* @param string $new_value New API key.
* @return bool True if verification succeeded.
*/
public static function trigger_site_verification( $old_value, $new_value ) {
return self::attempt_site_verification( sanitize_text_field( $new_value ) );
}
/**
* Triggers site verification when the API key is first added (not just updated).
*
* @param string $option Option name.
* @param string $value Option value.
* @return bool True if verification succeeded.
*/
public static function trigger_site_verification_on_add( $option, $value ) {
return self::attempt_site_verification( sanitize_text_field( $value ) );
}
/**
* Admin action handler: manual "Verify Now" button.
*
* @return void
*/
public static function handle_verify_site() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'pressnative-apps' ) );
}
check_admin_referer( 'pressnative_verify_site' );
$api_key = get_option( self::OPTION_API_KEY, '' );
$result = self::attempt_site_verification( $api_key );
if ( ! $result ) {
$error = get_transient( 'pressnative_verify_error' );
if ( empty( $error ) ) {
set_transient( 'pressnative_verify_error', 'Unknown error during verification.', 60 );
}
}
wp_safe_redirect( admin_url( 'admin.php?page=pressnative' ) );
exit;
}
/**
* Attempts site verification with the Registry.
*
* Generates a one-time nonce, stores it as a transient, then calls the
* Registry's /api/verify-site endpoint. The Registry calls back to
* /wp-json/pressnative/v1/verify-ownership to confirm the nonce,
* proving this WordPress site has wp-admin access for the given API key.
*
* @param string $api_key The API key to verify with.
* @return bool True if verification succeeded.
*/
public static function attempt_site_verification( $api_key ) {
if ( empty( $api_key ) ) {
set_transient( 'pressnative_verify_error', 'No API key provided.', 5 * MINUTE_IN_SECONDS );
return false;
}
$nonce = wp_generate_password( 48, false );
set_transient( 'pressnative_verify_nonce', $nonce, 5 * MINUTE_IN_SECONDS );
$registry_url = self::get_registry_url();
$verify_url = rtrim( $registry_url, '/' ) . '/api/verify-site';
$site_url = home_url();
$response = wp_remote_post(
$verify_url,
array(
'timeout' => 15,
'headers' => array(
'Content-Type' => 'application/json',
'X-PressNative-API-Key' => $api_key,
),
'body' => wp_json_encode(
array(
'site_url' => $site_url,
'nonce' => $nonce,
)
),
)
);
if ( is_wp_error( $response ) ) {
set_transient(
'pressnative_verify_error',
sprintf( 'Could not reach Registry at %s: %s', $registry_url, $response->get_error_message() ),
5 * MINUTE_IN_SECONDS
);
return false;
}
$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( $code >= 200 && $code < 300 && ! empty( $data['ok'] ) ) {
delete_transient( 'pressnative_verify_error' );
update_option( 'pressnative_site_verified', current_time( 'mysql' ) );
return true;
}
$error_detail = ! empty( $data['error'] ) ? $data['error'] : '';
$hint = ! empty( $data['hint'] ) ? ' ' . $data['hint'] : '';
set_transient(
'pressnative_verify_error',
sprintf( 'Registry returned HTTP %d: %s%s', $code, $error_detail, $hint ),
5 * MINUTE_IN_SECONDS
);
return false;
}
/**
* Show cache invalidation notice when settings are updated.
*
* @return void
*/
public static function show_cache_invalidation_notice() {
$cache_data = get_transient( 'pressnative_cache_invalidated' );
if ( ! $cache_data ) {
return;
}
// Only show on PressNative admin pages
$screen = get_current_screen();
if ( ! $screen || strpos( $screen->id, 'pressnative' ) === false ) {
return;
}
$option = $cache_data['option'];
$old_value = $cache_data['old_value'];
$new_value = $cache_data['new_value'];
// Make the option name more user-friendly
$option_display = str_replace( array( 'pressnative_product_', '_' ), array( '', ' ' ), $option );
$option_display = ucwords( $option_display );
?>
get_header( 'X-PressNative-Registry-Key' );
$admin_key = defined( 'PRESSNATIVE_ADMIN_KEY' )
? PRESSNATIVE_ADMIN_KEY
: getenv( 'ADMIN_API_KEY' );
if ( is_string( $admin_key ) && '' !== $admin_key && is_string( $registry_key ) && hash_equals( $admin_key, $registry_key ) ) {
return true;
}
return current_user_can( 'manage_options' );
}
/**
* REST permission callback for POST /pressnative/v1/verify-ownership.
* Requires the single-use verification nonce stored by wp-admin.
*
* @param WP_REST_Request $request Request.
* @return true|WP_Error
*/
public static function permission_check_verify_ownership( WP_REST_Request $request ) {
$params = $request->get_json_params();
$nonce = is_array( $params ) && isset( $params['nonce'] )
? sanitize_text_field( (string) $params['nonce'] )
: '';
if ( '' === $nonce ) {
return new WP_Error( 'missing_nonce', 'nonce is required', array( 'status' => 400 ) );
}
$stored_nonce = get_transient( 'pressnative_verify_nonce' );
if ( empty( $stored_nonce ) || ! is_string( $stored_nonce ) || ! hash_equals( $stored_nonce, $nonce ) ) {
return new WP_Error( 'invalid_nonce', 'Verification nonce is invalid or expired', array( 'status' => 403 ) );
}
return true;
}
}