'; echo esc_html__('2WayMicropay plugin requires:', '2waymicropay'); echo '

'; echo ''; echo esc_html__('Please update your environment!', '2waymicropay'); echo '

'; } /** * Plugin activation hook */ function qixit_activate() { try { qixit_activate_internal(); } catch (\Throwable $e) { if (function_exists('qixit_debug_log')) { qixit_debug_log('qixit_activate exception: ' . $e->getMessage()); } // Do not rethrow — leave plugin activatable; schema upgrade can finish on admin_init. set_transient('qixit_activation_warning', $e->getMessage(), DAY_IN_SECONDS); } } /** * Activation work (tables, options). Kept separate so fatals/exceptions are contained. */ function qixit_activate_internal() { global $wpdb, $wp_roles; if (!qixit_is_compatible()) { deactivate_plugins(plugin_basename(__FILE__)); wp_die(esc_html__('This plugin requires WordPress 5.0+, PHP 7.4+, and cURL extension.', '2waymicropay')); } $qixit_db_version = '2.5'; // Do not force users_can_register — membership sites manage this themselves. require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); $charset_collate = $wpdb->get_charset_collate(); // Create products table with GUID support $table_name = $wpdb->prefix . QIXIT_PRODUCTS; $sql = "CREATE TABLE IF NOT EXISTS " . $table_name . " ( `product_id` bigint(20) NOT NULL AUTO_INCREMENT, `post_id` bigint(20) DEFAULT NULL, `post_qixit_PID` varchar(255) DEFAULT NULL, `post_qixit_PID_guid` char(36) DEFAULT NULL, `qixit_post_type` enum('" . QIXIT_REG . "','" . QIXIT_PREMIUM_PAY_ONCE_VIEW_ANYTIME . "','" . QIXIT_PREMIUM_PAY_PER_VIEW . "') NOT NULL DEFAULT '" . QIXIT_REG . "', `premium_post_cost` decimal(15,4) DEFAULT NULL, `premium_post_url` varchar(500) DEFAULT NULL, `comments_qixit_PID` varchar(255) DEFAULT NULL, `premium_comments_cost` decimal(15,4) DEFAULT NULL, `percent_to_author` decimal(15,2) DEFAULT NULL, `use_readmore` tinyint(1) DEFAULT 0, `enable_paid_comments` tinyint(1) DEFAULT 0, `access_hours` int(11) DEFAULT -1 COMMENT '-1=forever, -2=pay per view, 0=session, N=hours', `commercial_category` varchar(50) DEFAULT 'CONTENT', `comments_qixit_PID_guid` char(36) DEFAULT NULL, `date_created` datetime DEFAULT NULL, `date_updated` datetime DEFAULT NULL, `migration_status` enum('pending','migrated','failed') DEFAULT 'pending', PRIMARY KEY (`product_id`), UNIQUE KEY `post_qixit_PID` (`post_qixit_PID`), KEY `idx_guid` (`post_qixit_PID_guid`), KEY `idx_post` (`post_id`) ) $charset_collate;"; dbDelta($sql); // Create payment details table $table_name = $wpdb->prefix . QIXIT_PAYMENT_DETAILS; $sql = "CREATE TABLE IF NOT EXISTS " . $table_name . " ( `payment_id` BIGINT NULL AUTO_INCREMENT, `product_id` BIGINT NULL, `type` enum('P','A') NOT NULL DEFAULT 'P', `qixit_PID` varchar(255) DEFAULT NULL, `qixit_PID_guid` char(36) DEFAULT NULL, `qixit_id` VARCHAR(60) DEFAULT NULL, `qixit_user_id` VARCHAR(100) DEFAULT NULL, `txn_id` VARCHAR(100) DEFAULT NULL, `total` decimal(15,4) NOT NULL, `payment_for` ENUM('author_registration','add_post','view_post','view_ad_hoc','comments') NOT NULL, `wp_user_id` BIGINT DEFAULT NULL, `percent_to_author` decimal(15,2) DEFAULT NULL, `date_purchased` DATETIME NULL DEFAULT NULL, PRIMARY KEY (`payment_id`), KEY `idx_guid` (`qixit_PID_guid`), KEY `idx_user` (`wp_user_id`), KEY `idx_qixit_user` (`qixit_user_id`), KEY `idx_txn` (`txn_id`) ) $charset_collate;"; dbDelta($sql); // Create author settings table $table_name = $wpdb->prefix . QIXIT_AUTHOR_SETTINGS; $sql = "CREATE TABLE IF NOT EXISTS " . $table_name . " ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `wp_user_id` int(11) NOT NULL, `qixit_id` varchar(255) DEFAULT NULL, `entitlement_status` varchar(30) DEFAULT 'inactive', `entitlement_purchased_at` datetime DEFAULT NULL, `entitlement_expires_at` datetime DEFAULT NULL, `date_created` datetime DEFAULT NULL, `date_updated` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user` (`wp_user_id`) ) $charset_collate;"; dbDelta($sql); // Set default options add_option('qixit_db_version', $qixit_db_version); add_option('qixit_api_timeout', 8); add_option('qixit_jwt_token', ''); add_option('qixit_jwt_expires', 0); add_option('qixit_vendor_id', 0); add_option('qixit_vendor_username', ''); add_option('qixit_enable_logging', false); add_option('qixit_development_mode', false); add_option('qixit_enable_author_commerce', false); add_option('qixit_show_frontend_credit', false); add_option('qixit_author_onboarding_fee', 0); add_option('qixit_author_publish_fee', 0); add_option('qixit_author_revision_fee', 0); add_option('qixit_author_charge_on_revision', false); add_option('qixit_author_default_split_percent', 0); add_option('qixit_author_require_moderation', true); add_option('qixit_author_terms_text', ''); add_option('qixit_author_signup_email_mode', 'required'); add_option('qixit_author_immediate_access_redirect_url', ''); add_option('qixit_share_comment_revenue_with_authors', false); add_option('qixit_comment_author_share_percent', 0); add_option('qixit_author_publication_product_guid', ''); add_option('qixit_author_revision_product_guid', ''); add_option('qixit_site_api_key', ''); add_option('qixit_enable_forwarder_commissions', false); add_option('qixit_forwarder_percent', 0); // Remove legacy option if exists (no longer user-configurable) delete_option('qixit_api_base_url'); delete_option('qixit_products_web_url'); // Add capabilities if ($wp_roles) { $wp_roles->add_cap('editor', 'qixit_settings_for_author'); $wp_roles->add_cap('author', 'qixit_settings_for_author'); } // One-time setup guidance after activation (never blocks activation). set_transient('qixit_show_setup_notice', 1, DAY_IN_SECONDS); // Defer rewrite flush — running it inline can fatal when other plugins/themes // hook rewrite registration during activate. set_transient('qixit_flush_rewrite_rules', 1, DAY_IN_SECONDS); } register_activation_hook(__FILE__, 'qixit_activate'); /** * Flush rewrite rules once after activation (safer than activate-time flush). */ function qixit_maybe_flush_rewrite_rules() { if (!is_admin() || !get_transient('qixit_flush_rewrite_rules')) { return; } delete_transient('qixit_flush_rewrite_rules'); flush_rewrite_rules(false); } add_action('admin_init', 'qixit_maybe_flush_rewrite_rules', 1); /** * Plugin deactivation hook */ function qixit_deactivate() { // Flush rewrite rules flush_rewrite_rules(); } register_deactivation_hook(__FILE__, 'qixit_deactivate'); /** * Check and upgrade database schema if needed * This runs on every admin page load to ensure columns exist */ function qixit_maybe_upgrade_db() { // Only run in admin if (!is_admin()) { return; } global $wpdb; $current_db_version = get_option('qixit_db_version', '1.0'); $required_db_version = '2.5'; // 2.5 adds author-commerce entitlement fields if (version_compare($current_db_version, $required_db_version, '>=')) { return; // Already up to date } $table_name = $wpdb->prefix . 'qixit_products'; $table_name_sql = esc_sql($table_name); // Check if table exists // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- schema existence check during controlled upgrade. $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)); if ($table_exists !== $table_name) { return; // Table doesn't exist, activation hook will create it } // Get existing columns $existing_columns = array(); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- table name is derived from $wpdb->prefix and escaped with esc_sql. $columns = $wpdb->get_results("SHOW COLUMNS FROM `{$table_name_sql}`"); foreach ($columns as $column) { $existing_columns[] = $column->Field; } // Add missing columns // access_hours: -1 = forever (pay once), -2 = pay per view, 0 = session only, N = hours of access $columns_to_add = array( 'use_readmore' => "ALTER TABLE {$table_name} ADD COLUMN `use_readmore` tinyint(1) DEFAULT 0", 'enable_paid_comments' => "ALTER TABLE {$table_name} ADD COLUMN `enable_paid_comments` tinyint(1) DEFAULT 0", 'migration_status' => "ALTER TABLE {$table_name} ADD COLUMN `migration_status` enum('pending','migrated','failed') DEFAULT 'pending'", 'access_hours' => "ALTER TABLE {$table_name} ADD COLUMN `access_hours` int(11) DEFAULT -1 COMMENT '-1=forever, -2=pay per view, 0=session, N=hours'", 'commercial_category' => "ALTER TABLE {$table_name} ADD COLUMN `commercial_category` varchar(50) DEFAULT 'CONTENT'", 'comments_qixit_PID_guid' => "ALTER TABLE {$table_name} ADD COLUMN `comments_qixit_PID_guid` char(36) DEFAULT NULL" ); foreach ($columns_to_add as $column_name => $sql) { if (!in_array($column_name, $existing_columns)) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- ALTER TABLE statements cannot be parameterized and are built from trusted constants. $wpdb->query($sql); if (function_exists('qixit_debug_log')) { qixit_debug_log("Added missing column: {$column_name}"); } } } // Migrate existing pay-per-view products to access_hours = 24 (default for PPV) if (!in_array('access_hours', $existing_columns)) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- table name is derived from $wpdb->prefix and escaped with esc_sql. $wpdb->query($wpdb->prepare("UPDATE `{$table_name_sql}` SET access_hours = 24 WHERE qixit_post_type = %s", QIXIT_PREMIUM_PAY_PER_VIEW)); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- table name is derived from $wpdb->prefix and escaped with esc_sql. $wpdb->query($wpdb->prepare("UPDATE `{$table_name_sql}` SET access_hours = -1 WHERE qixit_post_type = %s", QIXIT_PREMIUM_PAY_ONCE_VIEW_ANYTIME)); } // Ensure author settings table has entitlement columns. $author_table = $wpdb->prefix . QIXIT_AUTHOR_SETTINGS; $author_table_sql = esc_sql($author_table); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- schema existence check during controlled upgrade. if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $author_table)) === $author_table) { $author_columns = array(); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- table name is derived from $wpdb->prefix and escaped with esc_sql. $rows = $wpdb->get_results("SHOW COLUMNS FROM `{$author_table_sql}`"); foreach ($rows as $row) { $author_columns[] = $row->Field; } $author_columns_to_add = array( 'entitlement_status' => "ALTER TABLE {$author_table} ADD COLUMN `entitlement_status` varchar(30) DEFAULT 'inactive'", 'entitlement_purchased_at' => "ALTER TABLE {$author_table} ADD COLUMN `entitlement_purchased_at` datetime DEFAULT NULL", 'entitlement_expires_at' => "ALTER TABLE {$author_table} ADD COLUMN `entitlement_expires_at` datetime DEFAULT NULL" ); foreach ($author_columns_to_add as $column_name => $sql) { if (!in_array($column_name, $author_columns)) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- ALTER TABLE statements cannot be parameterized and are built from trusted constants. $wpdb->query($sql); } } } // Update version update_option('qixit_db_version', $required_db_version); } add_action('admin_init', 'qixit_maybe_upgrade_db'); /** * Initialize the plugin */ function qixit_init() { // Check compatibility if (!qixit_is_compatible()) { add_action('admin_notices', 'qixit_compatibility_notice'); return; } // WordPress.org loads translations automatically for hosted plugins. // Register and enqueue styles (filemtime so CTA CSS fixes cache-bust without a version bump) $admin_css_version = file_exists(QIXIT_PLUGIN_DIR . '/css/admin-style.css') ? (string) filemtime(QIXIT_PLUGIN_DIR . '/css/admin-style.css') : QIXIT_VERSION; $frontend_css_version = file_exists(QIXIT_PLUGIN_DIR . '/css/frontend-style.css') ? (string) filemtime(QIXIT_PLUGIN_DIR . '/css/frontend-style.css') : QIXIT_VERSION; wp_register_style('qixit-admin', QIXIT_PLUGIN_URL . '/css/admin-style.css', array(), $admin_css_version); wp_register_style('qixit-frontend', QIXIT_PLUGIN_URL . '/css/frontend-style.css', array(), $frontend_css_version); // Register and enqueue scripts $admin_js_version = file_exists(QIXIT_PLUGIN_DIR . '/js/admin-script.js') ? (string) filemtime(QIXIT_PLUGIN_DIR . '/js/admin-script.js') : QIXIT_VERSION; wp_register_script('qixit-admin', QIXIT_PLUGIN_URL . '/js/admin-script.js', array('jquery'), $admin_js_version, true); // Core SDK (hosted centrally so all sites get updates instantly, no jQuery needed) wp_register_script('qixit-sdk', 'https://products.qixit.com/sdk/qixit.js', array(), QIXIT_VERSION, true); // WordPress-specific extensions (AJAX purchase recording, shortcode handlers, pay-per-play/view) // Use filemtime so browser cache busts immediately after JS updates. $frontend_js_version = file_exists(QIXIT_PLUGIN_DIR . '/js/frontend-script.js') ? (string) filemtime(QIXIT_PLUGIN_DIR . '/js/frontend-script.js') : QIXIT_VERSION; wp_register_script('qixit-frontend', QIXIT_PLUGIN_URL . '/js/frontend-script.js', array('jquery', 'qixit-sdk'), $frontend_js_version, true); // Localize admin script only in wp-admin — avoids nonce generation // (which hits the DB) on every frontend page load. if (is_admin()) { $author_commerce_enabled = boolval(get_option('qixit_enable_author_commerce', false)); $requires_payment_gate_for_current_user = ( $author_commerce_enabled && is_user_logged_in() && current_user_can('edit_posts') && !current_user_can('manage_options') && !current_user_can('edit_others_posts') ); wp_localize_script('qixit-admin', 'qixitAdmin', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('qixit_admin_nonce'), 'authorCommerceEnabled' => $author_commerce_enabled, 'requiresPaymentGateForCurrentUser' => $requires_payment_gate_for_current_user, 'strings' => array( 'testing' => __('Testing connection...', '2waymicropay'), 'success' => __('Connection successful!', '2waymicropay'), 'error' => __('Connection failed', '2waymicropay') ) )); } } add_action('init', 'qixit_init'); /** * One-time safety repair (admin-only bootstrap): * - Ensure the primary account using admin_email has Administrator role * - Resolve duplicate user_email collisions that can break email login * * Reviewer note: runs once per site during init when an administrator is logged in; * not a public signup or role-elevation endpoint. */ function qixit_emergency_repair_admin_email_conflict() { if (!is_admin() || !current_user_can('manage_options')) { return; } if (get_option('qixit_emergency_admin_repair_done')) { return; } $admin_email = sanitize_email(get_option('admin_email', '')); if (empty($admin_email) || !is_email($admin_email)) { update_option('qixit_emergency_admin_repair_done', current_time('mysql')); return; } $users = get_users(array( 'search' => $admin_email, 'search_columns' => array('user_email'), 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'all', )); if (empty($users) || !is_array($users)) { update_option('qixit_emergency_admin_repair_done', current_time('mysql')); return; } $changed = false; $primary = $users[0]; $primary_user = get_user_by('id', intval($primary->ID)); if ($primary_user && !in_array('administrator', (array) $primary_user->roles, true)) { $primary_user->set_role('administrator'); $changed = true; } // Ensure only one account retains admin_email for deterministic email login. for ($i = 1; $i < count($users); $i++) { $u = $users[$i]; if (!isset($u->ID) || intval($u->ID) <= 0) { continue; } if (strcasecmp((string) $u->user_email, $admin_email) !== 0) { continue; } $base = sanitize_user((string) $u->user_login, true); if (empty($base)) { $base = 'user' . intval($u->ID); } $candidate = strtolower($base) . '+' . intval($u->ID) . '@no-email.qixit.local'; $suffix = 1; while (email_exists($candidate)) { $candidate = strtolower($base) . '+' . intval($u->ID) . '-' . $suffix . '@no-email.qixit.local'; $suffix++; } $update = wp_update_user(array( 'ID' => intval($u->ID), 'user_email' => $candidate, )); if (!is_wp_error($update)) { $changed = true; } } if ($changed && class_exists('Qixit_Logger')) { Qixit_Logger::get_instance()->log('Emergency admin/email conflict repair applied for admin_email=' . $admin_email, 'warning'); } update_option('qixit_emergency_admin_repair_done', current_time('mysql')); } add_action('init', 'qixit_emergency_repair_admin_email_conflict', 1); /** * Enqueue the 2WayMicropay Image block extension script for Gutenberg editor * This extends the native core/image block with micropay settings */ function qixit_enqueue_block_editor_assets() { // Only load if we have the block editor if (!function_exists('register_block_type')) { return; } // Check if helper functions are available (loaded from qixit-functions.php) if (!function_exists('qixit_get_products_api_url') || !function_exists('qixit_get_products_web_url')) { // Functions not loaded yet - use default URLs $productsApiUrl = 'https://products.qixit.com'; $productsWebUrl = 'https://products.qixit.com'; } else { $productsApiUrl = qixit_get_products_api_url(); $productsWebUrl = qixit_get_products_web_url(); } // Enqueue the extension script wp_enqueue_script( 'qixit-image-block-extension', QIXIT_PLUGIN_URL . '/blocks/qixit-image-block.js', array('wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-hooks', 'wp-compose'), QIXIT_VERSION, true ); // Localize script with API URLs wp_localize_script('qixit-image-block-extension', 'qixitBlockData', array( 'productsApiUrl' => $productsApiUrl, 'productsWebUrl' => $productsWebUrl, 'logoUrl' => function_exists('qixit_get_branding_logo_url') ? qixit_get_branding_logo_url() : '' )); // Enqueue editor styles wp_enqueue_style( 'qixit-image-block-editor', QIXIT_PLUGIN_URL . '/blocks/qixit-image-block.css', array('wp-edit-blocks'), QIXIT_VERSION ); } add_action('enqueue_block_editor_assets', 'qixit_enqueue_block_editor_assets'); /** * Build class/style attribute values for micropay image wrapper from block or shortcode attrs. * * @param array $attrs Block or shortcode attributes. * @return array{class:string,style:string} */ function qixit_build_micropay_image_container_attrs($attrs) { $classes = array('qixit-image-container'); $styles = array(); if (!empty($attrs['align'])) { $align = sanitize_key($attrs['align']); if (in_array($align, array('left', 'right', 'center', 'wide', 'full'), true)) { $classes[] = 'align' . $align; } } if (!empty($attrs['width'])) { $width = $attrs['width']; if (is_numeric($width)) { $styles[] = 'max-width:' . (int) $width . 'px'; } else { $styles[] = 'max-width:' . preg_replace('/[^0-9a-z.%\-]/i', '', (string) $width); } } if (!empty($attrs['max_width'])) { $max_width = $attrs['max_width']; if (is_numeric($max_width)) { $styles[] = 'max-width:' . (int) $max_width . 'px'; } else { $styles[] = 'max-width:' . preg_replace('/[^0-9a-z.%\-]/i', '', (string) $max_width); } } return array( 'class' => implode(' ', $classes), 'style' => !empty($styles) ? implode(';', $styles) : '', ); } /** * Process image blocks with micropay attributes on frontend render * Wraps the image in a link with overlay */ function qixit_process_micropay_images($block_content, $block) { // Only process core/image blocks if ($block['blockName'] !== 'core/image') { return $block_content; } // Check if micropay is enabled $attrs = $block['attrs']; if (empty($attrs['micropayEnabled']) || empty($attrs['micropayLink'])) { return $block_content; } // This page has a payment image — make sure Qixit assets are loaded if (class_exists('Qixit_Frontend')) { Qixit_Frontend::require_assets(); } // If the image is already wrapped in an (common for linked core/image // blocks), unwrap it before we add our own link. Nested tags are invalid // HTML; browsers "repair" them by closing early, which ejects our price // overlay out of the link and leads to a second JS-injected badge. // Prefer a Products Portal href from that existing when micropayLink is // missing a PID/PYID (some posts store a media URL in the block attribute). $linked_portal_href = ''; if (preg_match('/]*\bhref=(["\'])([^"\']+)\1[^>]*>\s*]*>\s*(]*>)\s*<\/a>/i', function ($matches) { return $matches[1]; }, $block_content ); $link = esc_url($attrs['micropayLink']); $has_portal_guid = (bool) preg_match('/(?:purchasefast\?PID=|paymentfast\?PYID=|[?&](?:PID|PYID)=)/i', $link); if (!$has_portal_guid && !empty($linked_portal_href)) { $link = esc_url($linked_portal_href); } // Always detect link type from the Products Portal URL. // Do not trust a saved micropayLinkType override (it can disagree with PID vs PYID). $linkType = ''; if (stripos($link, 'paymentfast') !== false || stripos($link, 'pyid=') !== false) { $linkType = 'payment'; } elseif (stripos($link, 'purchasefast') !== false || stripos($link, 'pid=') !== false) { $linkType = 'purchase'; } else { $linkType = 'purchase'; } $isPayment = ($linkType === 'payment'); // Extract product/payout GUID from the link to fetch price from API $itemGuid = ''; if (preg_match('/purchasefast\?PID=([a-f0-9\-]+)/i', $link, $matches)) { $itemGuid = $matches[1]; } elseif (preg_match('/paymentfast\?PYID=([a-f0-9\-]+)/i', $link, $matches)) { $itemGuid = $matches[1]; } elseif (preg_match('/[?&]PID=([a-f0-9\-]+)/i', $link, $matches)) { $itemGuid = $matches[1]; } elseif (preg_match('/[?&]PYID=([a-f0-9\-]+)/i', $link, $matches)) { $itemGuid = $matches[1]; } // For payment links, check if payout is available (has budget, not expired) // If not available, don't show the overlay (would be a false promise) if ($isPayment && $itemGuid && function_exists('qixit_check_payout_availability')) { $availability = qixit_check_payout_availability($itemGuid); if (!$availability['available']) { // Payout not available (budget exceeded, expired, etc.) // Return the original image without micropay overlay return $block_content; } } // Fetch price from API based on link type (product vs payout) $price = 0; $isPromo = false; if ($itemGuid) { if ($isPayment && function_exists('qixit_get_payout_info')) { // Payment links = payouts, use payout info API (includes payment type) $payout_info = qixit_get_payout_info($itemGuid); $price = $payout_info['price']; $isPromo = ($payout_info['paymentType'] === 1); } elseif ($isPayment && function_exists('qixit_get_payout_price')) { // Fallback: Payment links = payouts, use payout price API $price = qixit_get_payout_price($itemGuid); } elseif (function_exists('qixit_get_product_price')) { // Purchase links = products, use product price API $price = qixit_get_product_price($itemGuid); } } // Format price display (use HTML entity for cent symbol to avoid encoding issues) $priceDisplay = ''; if ($price > 0) { if ($price < 1.00) { $priceDisplay = round($price * 100) . '¢'; } else { $priceDisplay = '$' . number_format($price, 2); } if ($isPayment) { $priceDisplay = '+ ' . $priceDisplay; // Add "vendor credits" suffix for vendor-credit payouts if ($isPromo) { $priceDisplay .= ' vendor credits'; } } else { $priceDisplay = '- ' . $priceDisplay; } } // Determine overlay class (credit = payment received, debit = purchase made) $overlayClass = $isPayment ? 'credit' : 'debit'; if ($isPromo) { $overlayClass .= ' promo'; } // Resolve logo from branding manifest only. $logo_url = function_exists('qixit_get_branding_logo_url') ? qixit_get_branding_logo_url() : ''; if (empty($logo_url) && defined('QIXIT_PLUGIN_URL')) { $logo_url = QIXIT_PLUGIN_URL . '/images/overlay-logo.svg'; } $overlayPriceDisplay = $priceDisplay ? $priceDisplay : ($isPayment ? '+ click to earn' : '- click to purchase'); // Build tooltip text if ($isPayment) { $tooltip = $isPromo ? __('Click to earn', '2waymicropay') : __('Click to earn credits', '2waymicropay'); } else { $tooltip = __('Click to purchase', '2waymicropay'); } $tooltip_html = $tooltip; if ($priceDisplay) { $tooltip_html .= '
' . $priceDisplay; $tooltip .= ' ' . $priceDisplay; } $link_with_source = function_exists('qixit_append_source_page_param') ? qixit_append_source_page_param($link) : $link; $container_attrs = qixit_build_micropay_image_container_attrs($attrs); $container_class = $container_attrs['class']; $container_style = $container_attrs['style']; // Build the overlay HTML with logo and price (using same classes as shortcode) // Always show the overlay if micropay is enabled (even if price is 0, show the logo) $overlayHtml = '
'; if (!empty($logo_url)) { $overlayHtml .= ''; } $overlayHtml .= '' . esc_html($overlayPriceDisplay) . ''; $overlayHtml .= '
'; // Wrap the image in a container with link and overlay (using same classes as shortcode) if ($isPayment) { // Payment links open in new tab $linkClass = 'qixit-image-link qixit-payment-link qixit-tooltip' . ($isPromo ? ' qixit-promo-link' : ''); $wrapper = '
'; $wrapper .= ''; $wrapper .= $block_content; $wrapper .= $overlayHtml; $wrapper .= '' . wp_kses($tooltip_html, array('br' => array())) . ''; $wrapper .= '
'; } else { // Purchase links use microframe - include the actual URL in title for clarity $wrapper = '
'; $wrapper .= ''; $wrapper .= $block_content; $wrapper .= $overlayHtml; $wrapper .= '' . wp_kses($tooltip_html, array('br' => array())) . ''; $wrapper .= '
'; } return $wrapper; } add_filter('render_block', 'qixit_process_micropay_images', 10, 2); /** * Load required files */ qixit_debug_log('About to check compatibility'); if (qixit_is_compatible()) { qixit_debug_log('Compatible, loading files from: ' . QIXIT_PLUGIN_DIR); // Helper functions MUST load first (defines URL constants and helper functions) qixit_debug_log('Loading qixit-functions.php'); require_once QIXIT_PLUGIN_DIR . '/includes/qixit-functions.php'; qixit_debug_log('qixit-functions.php loaded'); // Core functionality qixit_debug_log('Loading class-qixit-logger.php'); require_once QIXIT_PLUGIN_DIR . '/includes/class-qixit-logger.php'; qixit_debug_log('Loading class-qixit-auth.php'); require_once QIXIT_PLUGIN_DIR . '/includes/class-qixit-auth.php'; qixit_debug_log('Loading class-qixit-api.php'); require_once QIXIT_PLUGIN_DIR . '/includes/class-qixit-api.php'; qixit_debug_log('Loading class-qixit-product.php'); require_once QIXIT_PLUGIN_DIR . '/includes/class-qixit-product.php'; qixit_debug_log('Loading class-qixit-payment.php'); require_once QIXIT_PLUGIN_DIR . '/includes/class-qixit-payment.php'; qixit_debug_log('Loading class-qixit-comments.php'); require_once QIXIT_PLUGIN_DIR . '/includes/class-qixit-comments.php'; qixit_debug_log('Loading class-qixit-author-commerce.php'); require_once QIXIT_PLUGIN_DIR . '/includes/class-qixit-author-commerce.php'; qixit_debug_log('Core files loaded'); // Admin functionality if (is_admin()) { qixit_debug_log('Loading admin files'); require_once QIXIT_PLUGIN_DIR . '/admin/class-qixit-admin.php'; qixit_debug_log('Loading class-qixit-metabox.php'); require_once QIXIT_PLUGIN_DIR . '/admin/class-qixit-metabox.php'; qixit_debug_log('Loading qixit-admin-ajax.php'); require_once QIXIT_PLUGIN_DIR . '/admin/qixit-admin-ajax.php'; qixit_debug_log('Admin files loaded'); } // Frontend functionality - Load for frontend AND for AJAX requests (admin-ajax.php) // is_admin() returns true for AJAX, so we need to check for AJAX separately $qixit_is_ajax = defined('DOING_AJAX') && DOING_AJAX; if (!is_admin() || $qixit_is_ajax) { qixit_debug_log('Loading frontend files (is_admin=' . (is_admin() ? 'Y' : 'N') . ', is_ajax=' . ($qixit_is_ajax ? 'Y' : 'N') . ')'); require_once QIXIT_PLUGIN_DIR . '/public/class-qixit-frontend.php'; // Initialize the frontend singleton to hook up all actions/filters Qixit_Frontend::get_instance(); qixit_debug_log('Frontend files loaded and initialized'); } // Shortcodes - load both for frontend and admin (admin needs them for block preview) qixit_debug_log('Loading qixit-shortcodes.php'); require_once QIXIT_PLUGIN_DIR . '/public/qixit-shortcodes.php'; require_once QIXIT_PLUGIN_DIR . '/public/qixit-ad-unit.php'; qixit_debug_log('Shortcodes loaded'); qixit_debug_log('All files loaded successfully'); } else { qixit_debug_log('Not compatible - skipping file loading'); } /** * Add settings link on plugin page */ function qixit_plugin_action_links($links, $file) { if ($file != QIXIT_PLUGIN_BASENAME) { return $links; } $settings_link = '' . __('Settings', '2waymicropay') . ''; array_unshift($links, $settings_link); return $links; } add_filter('plugin_action_links', 'qixit_plugin_action_links', 10, 2); /** * Display admin notice if Site API Key is not configured. * Null-safe for get_current_screen() so activation/admin bootstrap cannot fatal on PHP 8+. */ function qixit_admin_notice_not_configured() { if (!current_user_can('manage_options')) { return; } $activation_warning = get_transient('qixit_activation_warning'); if ($activation_warning) { delete_transient('qixit_activation_warning'); echo '

' . esc_html__('2WayMicropay', '2waymicropay') . ': '; echo esc_html__('Activated with a non-fatal setup warning. Settings should still work. Detail:', '2waymicropay') . ' '; echo esc_html((string) $activation_warning); echo '

'; } $site_key = trim((string) get_option('qixit_site_api_key', '')); if ($site_key === '') { $site_key = trim((string) get_option('qixit_developer_api_key', '')); } if ($site_key !== '') { if (get_transient('qixit_show_setup_notice')) { delete_transient('qixit_show_setup_notice'); } return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if ($screen && isset($screen->id) && $screen->id === 'toplevel_page_qixit-settings') { return; } $is_post_activate = (bool) get_transient('qixit_show_setup_notice'); if ($is_post_activate) { delete_transient('qixit_show_setup_notice'); } echo '

' . esc_html__('2WayMicropay', '2waymicropay') . ': '; if ($is_post_activate) { echo esc_html__('Plugin activated successfully. Add your Site API Key in', '2waymicropay') . ' '; } else { echo esc_html__('Add your Site API Key in', '2waymicropay') . ' '; } echo '' . esc_html__('Settings', '2waymicropay') . ''; echo ' ' . esc_html__('to connect this site.', '2waymicropay') . ' '; echo '' . esc_html__('Create a Site API Key', '2waymicropay') . '.'; echo '

'; } add_action('admin_notices', 'qixit_admin_notice_not_configured'); /** * Unwrap qixit shortcodes from inline wrappers so block-level shortcode * output does not leave theme-styled leftover shells in the content. * * Note: do not use variable-length lookbehinds here — PCRE rejects them and * preg_replace_callback can return null, which empties the_content. */ function qixit_unwrap_shortcodes_from_inline_code($content) { if (!is_string($content) || $content === '' || strpos($content, '[') === false) { return $content; } $updated = preg_replace_callback( '#]*)?>\s*(\[(?:qixit_[a-z0-9_]+|2waymicropay_image)(?:\s[^\]]*)?/?\])\s*#i', static function ($matches) { return $matches[1]; }, $content ); // If PCRE fails, keep original content rather than wiping the post. return is_string($updated) ? $updated : $content; } add_filter('the_content', 'qixit_unwrap_shortcodes_from_inline_code', 9); // Plugin loaded successfully do_action('qixit_loaded'); // NOTE: The core/image block extension is registered via qixit_enqueue_block_editor_assets() // We do NOT register a separate qixit/image block to avoid duplicate panels in the editor /** * Register TinyMCE plugin for Classic Editor */ function qixit_register_tinymce_plugin($plugins) { $plugins['qixit_image'] = QIXIT_PLUGIN_URL . '/js/tinymce-qixit-image.js'; return $plugins; } /** * Add TinyMCE button to the editor toolbar */ function qixit_add_tinymce_button($buttons) { array_push($buttons, 'qixit_image'); return $buttons; } /** * Initialize TinyMCE plugin for Classic Editor */ function qixit_init_tinymce() { // Only for users with editing capability if (!current_user_can('edit_posts') && !current_user_can('edit_pages')) { return; } // Only if rich editing is enabled if (get_user_option('rich_editing') !== 'true') { return; } // Add the TinyMCE plugin and button add_filter('mce_external_plugins', 'qixit_register_tinymce_plugin'); add_filter('mce_buttons', 'qixit_add_tinymce_button'); } add_action('admin_init', 'qixit_init_tinymce'); /** * Enqueue media library script for TinyMCE plugin */ function qixit_enqueue_tinymce_media() { if (!current_user_can('edit_posts') && !current_user_can('edit_pages')) { return; } // Ensure media library is available wp_enqueue_media(); } add_action('admin_enqueue_scripts', 'qixit_enqueue_tinymce_media');