Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 47 additions & 9 deletions assets/js/paybutton-paywall-cashtab-login.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ let isLoggedIn = false;

/**
* Handle user login:
* Called when the PayButton payment returns a successful login transaction.
*/
function handleLogin(address) {
* Called when the PayButton login flow completes successfully.
*/
function handleLogin(address, txHash, loginToken) {
isLoggedIn = true;
jQuery.post(
PaywallAjax.ajaxUrl,
{
action: 'paybutton_save_address',
security: PaywallAjax.nonce,
address: address
address: address,
tx_hash: txHash,
login_token: loginToken
},
function() {
var baseUrl = location.href.split('?')[0];
Expand All @@ -26,7 +28,7 @@ function handleLogin(address) {

/**
* Handle user logout.
*/
*/
function handleLogout() {
jQuery.post(
PaywallAjax.ajaxUrl,
Expand All @@ -44,10 +46,12 @@ function handleLogout() {
/**
* Render the "Login via Cashtab" PayButton.
* (5.5 XEC is hard-coded.)
*/
*/
function renderLoginPaybutton() {
// Shared state: login address captured in onSuccess, consumed in onClose.
let loginAddr = null;
let loginTx = null;

PayButton.render(document.getElementById('loginPaybutton'), {
to: PaywallAjax.defaultAddress,
amount: 5.5,
Expand All @@ -56,15 +60,49 @@ function renderLoginPaybutton() {
hoverText: 'Click to Login',
successText: 'Login Successful!',
autoClose: true,
opReturn: 'login',
onSuccess: function (tx) {
loginAddr = tx?.inputAddresses?.[0] ?? null;
loginTx = {
hash: tx?.hash ?? '',
timestamp: tx?.timestamp ?? 0
};
},
onClose: function () {
if (loginAddr) {
handleLogin(loginAddr);
if (loginAddr && loginTx && loginTx.hash) {
// Make stable copies for the whole retry flow
const addrCopy = loginAddr;
const hashCopy = loginTx.hash;

function tryValidateLogin(attempt) {
jQuery.post(
PaywallAjax.ajaxUrl,
{
action: 'validate_login_tx',
security: PaywallAjax.nonce,
wallet_address: addrCopy,
tx_hash: hashCopy
},
function (resp) {
if (resp && resp.success && resp.data && resp.data.login_token) {
// Pass the random token from the server
handleLogin(addrCopy, hashCopy, resp.data.login_token);
} else {
if (attempt === 1) {
// Retry once again after 3 seconds
setTimeout(() => tryValidateLogin(2), 3000);
} else {
alert('⚠️ Login failed: Invalid or expired transaction.');
}
}
}
);
}
tryValidateLogin(1);
}
// Prevent stale reuse on subsequent opens
// Safe to clear shared state (the flow above uses the copies)
loginAddr = null;
loginTx = null;
}
});
}
Expand Down
152 changes: 106 additions & 46 deletions assets/js/paywalled-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,77 +24,137 @@ jQuery(document).ready(function($) {
$('.paybutton-container').each(function() {
var $container = $(this);
var configData = $container.data('config');

if (typeof configData === 'string') {
try {
configData = JSON.parse(configData);
} catch(e) {
} catch (e) {
console.error('Invalid JSON in paybutton-container data-config');
return;
}
}
configData.onSuccess = function(tx) {
$.ajax({

// Shared state: user wallet address + unlock tx captured in onSuccess, consumed in onClose.
let unlockAddr = null;
let unlockTx = null;

// Helper to fetch and inject unlocked content
function fetchUnlocked() {
jQuery.ajax({
method: 'POST',
url: PaywallAjax.ajaxUrl,
data: {
action: 'mark_payment_successful',
action: 'fetch_unlocked_content',
post_id: configData.postId,
security: PaywallAjax.nonce,
tx_hash: tx.hash || '',
tx_amount: tx.amount || '',
tx_timestamp: tx.timestamp || '',
// NEW: Pass the first input address to store in the DB even for non-logged-in users
user_address: (tx.inputAddresses && tx.inputAddresses.length > 0) ? tx.inputAddresses[0] : '',
autoClose: configData.autoClose
security: PaywallAjax.nonce
},
success: function () {
setTimeout(function () {
jQuery.ajax({
method: 'POST',
url: PaywallAjax.ajaxUrl,
data: {
action: 'fetch_unlocked_content',
post_id: configData.postId,
security: PaywallAjax.nonce
},
success: function (resp) {
if (resp && resp.success) {
// 1) Replace only the paywalled block content
var $wrapper = jQuery('#pb-paywall-' + configData.postId);
if ($wrapper.length && resp.data.unlocked_html) {
$wrapper.html(resp.data.unlocked_html);
}
success: function (resp) {
if (resp && resp.success) {
var $wrapper = jQuery('#pb-paywall-' + configData.postId);
if ($wrapper.length && resp.data.unlocked_html) {
$wrapper.html(resp.data.unlocked_html);
}

// Optional scroll-to-unlocked-content-indicator + Cache Busting Mechanism
var baseUrl = location.href.split('#')[0].split('?')[0];
var newUrl = baseUrl + '?t=' + Date.now() + '#unlocked';
window.history.replaceState(null, '', newUrl);
// Cache bust + scroll to unlocked content indicator
var baseUrl = location.href.split('#')[0].split('?')[0];
var newUrl = baseUrl + '?t=' + Date.now() + '#unlocked';
window.history.replaceState(null, '', newUrl);

if (PaywallAjax.scrollToUnlocked === '1' || PaywallAjax.scrollToUnlocked === 1) {
var $target = jQuery('#unlocked');
if ($target.length) {
var headerOffset = 80;
jQuery('html, body').animate({ scrollTop: $target.offset().top - headerOffset }, 500);
}
}
}
if (PaywallAjax.scrollToUnlocked === '1' || PaywallAjax.scrollToUnlocked === 1) {
var $target = jQuery('#unlocked');
if ($target.length) {
var headerOffset = 80;
jQuery('html, body').animate({ scrollTop: $target.offset().top - headerOffset }, 500);
}
});
}, 20); // Slight delay to ensure DB/cookie update is processed before fetching content
}
}
}
});
};
}

// Configure the PayButton like before, but:
// - onSuccess only captures tx data
// - onClose does the secure validate -> mark_payment_successful -> fetch flow
PayButton.render($container[0], {
to: configData.to,
amount: configData.amount,
currency: configData.currency,
text: configData.buttonText,
hoverText: configData.hoverText,
successText: configData.successText,
onSuccess: configData.onSuccess,
theme: configData.theme,
opReturn: configData.opReturn, //This is a hack to give the PB server the post ID to send it back to WP's DB
autoClose: configData.autoClose
opReturn: configData.opReturn, // carries postID
autoClose: configData.autoClose,

onSuccess: function (tx) {
unlockAddr = (tx.inputAddresses && tx.inputAddresses.length > 0)
? tx.inputAddresses[0]
: '';
unlockTx = {
hash: tx.hash || '',
amount: tx.amount || '',
timestamp: tx.timestamp || 0
};
},

onClose: function () {
if (unlockAddr && unlockTx && unlockTx.hash) {
const addrCopy = unlockAddr;
const hashCopy = unlockTx.hash;
const amtCopy = unlockTx.amount;
const tsCopy = unlockTx.timestamp;
const postIdCopy = configData.postId;

function tryValidateUnlock(attempt) {
jQuery.post(
PaywallAjax.ajaxUrl,
{
action: 'validate_unlock_tx',
security: PaywallAjax.nonce,
wallet_address: addrCopy,
tx_hash: hashCopy,
post_id: postIdCopy
},
function (resp) {
if (resp && resp.success && resp.data && resp.data.unlock_token) {
// We have a server-issued token – now mark payment as successful.
jQuery.ajax({
method: 'POST',
url: PaywallAjax.ajaxUrl,
data: {
action: 'mark_payment_successful',
post_id: postIdCopy,
security: PaywallAjax.nonce,
tx_hash: hashCopy,
tx_amount: amtCopy,
tx_timestamp: tsCopy,
user_address: addrCopy,
unlock_token: resp.data.unlock_token
},
success: function () {
// Finally, fetch and render the unlocked content
fetchUnlocked();
}
});
Comment on lines +121 to +138
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for mark_payment_successful AJAX call.

The mark_payment_successful call has no error handler. If this request fails (e.g., network issue, server error), the user receives no feedback despite having a valid token. The payment would be validated but the content wouldn't unlock.

                                     jQuery.ajax({
                                         method: 'POST',
                                         url: PaywallAjax.ajaxUrl,
                                         data: {
                                             action: 'mark_payment_successful',
                                             post_id: postIdCopy,
                                             security: PaywallAjax.nonce,
                                             tx_hash: hashCopy,
                                             tx_amount: amtCopy,
                                             tx_timestamp: tsCopy,
                                             user_address: addrCopy,
                                             unlock_token: resp.data.unlock_token
                                         },
                                         success: function () {
                                             fetchUnlocked();
-                                        }
+                                        },
+                                        error: function () {
+                                            alert('⚠️ Failed to complete unlock. Please refresh the page and try again.');
+                                        }
                                     });
🤖 Prompt for AI Agents
In assets/js/paywalled-content.js around lines 121 to 138, the AJAX call to
action 'mark_payment_successful' lacks error handling; add an error (and/or
complete) callback to handle network/server failures by logging the error,
showing a user-facing message (e.g., "Unable to mark payment; retrying/unlock
may be delayed"), and ensuring the unlocked content is still attempted (call
fetchUnlocked() on success and consider calling it or queuing a retry in
error/complete so a valid unlock_token isn't wasted); ensure you include the
jqXHR/textStatus/errorThrown info in logs and avoid silent failures.

} else {
if (attempt === 1) {
// Retry once after brief delay
setTimeout(function () { tryValidateUnlock(2); }, 3000);
} else {
alert('⚠️ Payment could not be verified on-chain. Please try again.');
}
}
}
);
}

tryValidateUnlock(1);
}

// Safe to clear shared state (the flow above uses the copies)
unlockAddr = null;
unlockTx = null;
}
});
});
});
Expand Down
41 changes: 32 additions & 9 deletions includes/class-paybutton-activator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class PayButton_Activator {

/**
* Activation hook callback.
*/
*/
public static function activate() {
self::create_tables();
self::create_profile_page();
Expand All @@ -34,31 +34,54 @@ private static function migrate_old_option() {

/**
* Create the custom table for unlocked content.
*/
*/
public static function create_tables() {
global $wpdb; //$wpdb is WordPress’s way of interacting with the database, and it provides methods for running queries and getting the correct table prefix.

$table_name = $wpdb->prefix . 'paybutton_paywall_unlocked';
$charset_collate = $wpdb->get_charset_collate();

// Updated table definition with the new column name pb_paywall_user_wallet_address
// and index name pb_paywall_user_wallet_address_idx
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
// ---- PayButton Paywall Unlocks table ----
$table_name = $wpdb->prefix . 'paybutton_paywall_unlocked';

$sql = "CREATE TABLE $table_name (
id INT NOT NULL AUTO_INCREMENT,
pb_paywall_user_wallet_address VARCHAR(255) NOT NULL,
post_id BIGINT(20) UNSIGNED NOT NULL,
tx_hash VARCHAR(64) DEFAULT '',
tx_amount DECIMAL(20,8) DEFAULT 0,
tx_amount DECIMAL(20,2) DEFAULT 0,
tx_timestamp DATETIME DEFAULT '0000-00-00 00:00:00',
is_logged_in TINYINT(1) DEFAULT 0,
unlock_token VARCHAR(64) DEFAULT '',
used TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY pb_paywall_user_wallet_address_idx (pb_paywall_user_wallet_address),
KEY post_id_idx (post_id)
KEY post_id_idx (post_id),
KEY tx_hash_idx (tx_hash),
KEY unlock_token_idx (unlock_token)
) $charset_collate;";

require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );

// ---- PayButton Logins table ----
$login_table = $wpdb->prefix . 'paybutton_logins';

$sql_login = "CREATE TABLE $login_table (
id INT NOT NULL AUTO_INCREMENT,
wallet_address VARCHAR(255) NOT NULL,
tx_hash VARCHAR(64) NOT NULL,
tx_amount DECIMAL(20,2) NOT NULL,
tx_timestamp INT(11) NOT NULL,
login_token VARCHAR(64) DEFAULT '',
used TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY tx_hash_idx (tx_hash),
KEY wallet_addr_idx (wallet_address(190)),
KEY used_idx (used),
KEY login_token_idx (login_token)
) {$charset_collate};";

dbDelta( $sql_login );
}

/**
Expand Down
Loading