';
echo esc_html__('2WayMicropay plugin requires:', '2waymicropay');
echo '
';
echo '- ' . esc_html__('WordPress 5.0 or newer', '2waymicropay') . '
';
echo '- ' . esc_html__('PHP 7.4 or newer', '2waymicropay') . '
';
echo '- ' . esc_html__('cURL PHP extension', '2waymicropay') . '
';
echo '- ' . esc_html__('JSON PHP extension', '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 = '';
} else {
// Purchase links use microframe - include the actual URL in title for clarity
$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 '';
}
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');