Skip to content

Improve Content Unlock Speed#101

Merged
Klakurka merged 8 commits intomasterfrom
fix/improve-unlock-speed
Dec 7, 2025
Merged

Improve Content Unlock Speed#101
Klakurka merged 8 commits intomasterfrom
fix/improve-unlock-speed

Conversation

@xecdev
Copy link
Collaborator

@xecdev xecdev commented Dec 6, 2025

This PR implements #99 by moving the content unlock logic from the onClose callback to the onSuccess callback to improve UX.

Test plan

  • Install the plugin
  • Unlock content and test the speed.

Greptile Overview

Greptile Summary

This PR successfully implements issue #99 by moving content unlock logic from the onClose callback to the onSuccess callback, eliminating the delay caused by waiting for the PayButton modal to close.

Key Changes:

  • Content unlock now triggers immediately in onSuccess instead of waiting for onClose
  • Enhanced retry mechanism from 2 attempts to 5 attempts with optimized delays (1s initial delay, then 1.2s, 1s, 1s, 2s between retries)
  • Added console logging for better debugging of retry attempts

Minor Issues Found:

  • Inconsistent console log messages (attempts 4 and 5 both labeled as "Last one")

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk - the logic refactoring is straightforward and improves UX
  • The change successfully addresses the performance issue by eliminating unnecessary modal close delays. The core unlock flow remains unchanged (still validates via server, marks payment, fetches content). The enhanced retry logic (5 attempts vs 2) actually improves reliability. Only minor cosmetic issues found in console log messages.
  • No files require special attention - the single changed file has minor cosmetic issues only

Important Files Changed

File Analysis

Filename Score Overview
assets/js/paywalled-content.js 4/5 Moved content unlock from onClose to onSuccess callback and enhanced retry logic (5 attempts instead of 2) with optimized delays for faster unlock experience

Sequence Diagram

sequenceDiagram
    participant User
    participant PayButton
    participant onSuccess
    participant Server
    participant Content

    User->>PayButton: Complete Payment
    PayButton->>onSuccess: tx data (hash, address, amount)
    onSuccess->>onSuccess: Capture tx data
    onSuccess->>onSuccess: Wait 1000ms (initial delay)
    
    loop Retry Logic (up to 5 attempts)
        onSuccess->>Server: validate_unlock_tx (attempt N)
        alt Transaction validated
            Server-->>onSuccess: success + unlock_token
            onSuccess->>Server: mark_payment_successful
            Server-->>onSuccess: success
            onSuccess->>Server: fetch_unlocked_content
            Server-->>Content: unlocked HTML
            Content-->>User: Display unlocked content
        else Not yet validated
            Server-->>onSuccess: validation failed
            alt attempt < 5
                onSuccess->>onSuccess: Wait (1200ms-2000ms)
            else final attempt failed
                onSuccess-->>User: Alert: Payment verification failed
            end
        end
    end
Loading

Summary by CodeRabbit

  • Bug Fixes

    • Improved payment validation with initial delay and up to four automatic retries using incremental backoff for more reliable unlocks.
    • Aligned unlock fetching to occur after successful validation to reduce race conditions.
  • New Features

    • Automatic smooth scrolling to unlocked content when visiting pages with the "#unlocked" hash.
  • Chores

    • Removed legacy "close" callback option from the payment button configuration (integration impact).

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly relates to the main objective of the PR: improving the speed of content unlock by moving validation to the onSuccess callback instead of waiting for modal close.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/improve-unlock-speed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@xecdev xecdev self-assigned this Dec 6, 2025
@xecdev xecdev added the enhancement (UI/UX/feature) New feature or request label Dec 6, 2025
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
assets/js/paywalled-content.js (1)

11-21: Update high-level comments and remove obsolete “shared state” pattern

The implementation no longer reloads the page after 2 seconds; instead it performs validate_unlock_txmark_payment_successfulfetchUnlocked() entirely from onSuccess, updating the DOM in place. The header comment (lines 17–20) and the “Shared state … consumed in onClose” comment (line 37) are now inaccurate, and unlockAddr / unlockTx are no longer used outside onSuccess.

Consider:

  • Updating the top-of-file “How it works” steps to describe the current validate+fetch flow instead of a 2s reload.
  • Making unlockAddr / unlockTx local constants inside onSuccess and removing the now-misleading “shared state” comment and the cleanup at lines 194–196.

This keeps future readers from assuming there is still an onClose-based flow or cross-callback shared state.

Also applies to: 37-40, 97-120, 194-197

🧹 Nitpick comments (2)
assets/js/paywalled-content.js (2)

127-181: Retry chain is correct but repetitive and could better distinguish transient vs permanent failures

The multi-step retry (tryValidateUnlock attempts 1→5 with increasing delays) is logically correct and bounded, but:

  • The branching on attempt === 1/2/3/4 is repetitive and slightly brittle if you ever want to tweak timings or number of attempts.
  • All non-success responses (including potential permanent failures) are treated as transient; the client keeps retrying until attempt 5, then only shows the alert. If the backend can differentiate “not yet unlocked” vs. “hard failure”, you might want to short‑circuit retries on explicit permanent errors later.

A simpler, more maintainable pattern would be to define an array of delays and compute the next attempt, e.g.:

-                    function tryValidateUnlock(attempt) {
+                    const unlockRetryDelaysMs = [1200, 1000, 1000, 2000]; // attempts 2–5
+
+                    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) {
                                     // ...
                                 } else {
-                                    if (attempt === 1) {
-                                        setTimeout(function () { tryValidateUnlock(2); }, 1200);
-                                        console.log('Retrying unlock validation (attempt 2)...');
-                                    }
-                                    else if(attempt === 2) {
-                                        setTimeout(function () { tryValidateUnlock(3); }, 1000);
-                                        console.log('Retrying unlock validation (attempt 3)...');
-                                    }
-                                    else if(attempt === 3) {
-                                        setTimeout(function () { tryValidateUnlock(4); }, 1000);
-                                        console.log('Retrying unlock validation (attempt 4 Last one)...');
-                                    }
-                                    else if (attempt === 4) {
-                                        setTimeout(function () { tryValidateUnlock(5); }, 2000);
-                                        console.log('Retrying unlock validation (attempt 5 Last one)...');
-                                    }
-                                    else {
+                                    const nextIndex = attempt - 1;
+                                    if (nextIndex < unlockRetryDelaysMs.length) {
+                                        const nextAttempt = attempt + 1;
+                                        const delay = unlockRetryDelaysMs[nextIndex];
+                                        setTimeout(function () { tryValidateUnlock(nextAttempt); }, delay);
+                                        console.log('Retrying unlock validation (attempt ' + nextAttempt + ')...');
+                                    } else {
                                         alert('⚠️ Payment could not be verified on-chain. Please try again.');
                                     }
                                 }
                             }
                         );
                     }

Also consider adding an explicit error/fail handler to jQuery.post so that network errors are treated similarly to non-success responses instead of silently hanging.


187-191: Initial 1s delay is reasonable but should be treated as a tunable constant

The initial setTimeout(..., 1000) before tryValidateUnlock(1) aligns with the goal of giving the webhook time to record the transaction, but it is currently a magic number embedded in the callback and comment (“selected experimentally”).

To keep behavior easier to tune and document over time, consider extracting this into a named constant or config value near tryValidateUnlock, e.g. const initialUnlockDelayMs = 1000; and reusing it. That will make it clearer what to tweak if unlock validation needs to be sped up or slowed down in different environments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c966c8 and c434ce9.

📒 Files selected for processing (1)
  • assets/js/paywalled-content.js (2 hunks)

xecdev and others added 2 commits December 6, 2025 08:50
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@xecdev xecdev requested a review from Klakurka December 6, 2025 04:23
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
assets/js/paywalled-content.js (1)

16-21: Update outdated documentation.

The comment describes the old flow that reloaded the page after 2 seconds. The current implementation validates the transaction, marks the payment as successful, and dynamically fetches and injects the unlocked content without reloading the page.

Apply this diff to update the documentation:

-*      - It then sets up an onSuccess callback. When the payment is successful, this callback:
-*           a) Sends an AJAX POST request (using jQuery's $.ajax) to the server endpoint 
-*              (action: "mark_payment_successful") with the transaction details.
-*           b) On success, waits 2000 ms (2 seconds) and then reloads the page so that
-*              the now unlocked content is displayed.
+*      - It then sets up an onSuccess callback. When the payment is successful, this callback:
+*           a) Validates the transaction with the server (action: "validate_unlock_tx") with retry logic.
+*           b) Marks the payment as successful (action: "mark_payment_successful") with the unlock token.
+*           c) Fetches and dynamically injects the unlocked content without reloading the page.
🧹 Nitpick comments (2)
assets/js/paywalled-content.js (2)

37-39: Consider making these local variables within onSuccess.

Since the onClose callback has been removed, these variables are no longer shared between callbacks. They're only used within onSuccess and are immediately copied to local variables (addrCopy, hashCopy, etc.). Making them local to onSuccess would improve clarity and reduce scope.

-        // 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

Then inside the onSuccess callback at line 110:

             onSuccess: function (tx) {
-                unlockAddr = (tx.inputAddresses && tx.inputAddresses.length > 0)
+                const unlockAddr = (tx.inputAddresses && tx.inputAddresses.length > 0)
                     ? tx.inputAddresses[0]
                     : '';
-                unlockTx = {
+                const unlockTx = {
                     hash: tx.hash || '',
                     amount: tx.amount || '',
                     timestamp: tx.timestamp || 0
                 };
 
                 if (unlockAddr && unlockTx && unlockTx.hash) {
                     const addrCopy   = unlockAddr;
                     const hashCopy   = unlockTx.hash;
                     const amtCopy    = unlockTx.amount;
                     const tsCopy     = unlockTx.timestamp;
                     const postIdCopy = configData.postId;

And remove lines 194-196 since the variables would be automatically scoped to the function.


159-178: Consider refactoring retry logic to reduce duplication.

The retry logic uses four nearly identical if-else blocks, which violates the DRY principle. This can be simplified using an array of delays.

                                 } else {
-                                    if (attempt === 1) {
-                                        setTimeout(function () { tryValidateUnlock(2); }, 1200); // 1.2s delay to give the PayButton webhook time
-                                    }
-                                    else if(attempt === 2) {
-                                        setTimeout(function () { tryValidateUnlock(3); }, 1000); // 1s delay to give the PayButton webhook time
-                                    }
-                                    else if(attempt === 3) {
-                                        setTimeout(function () { tryValidateUnlock(4); }, 1000); // 1s delay to give the PayButton webhook time
-                                    }
-                                    else if (attempt === 4) {
-                                        setTimeout(function () { tryValidateUnlock(5); }, 2000); // 2s delay to give the PayButton webhook time
-                                    }
-                                    else {
+                                    // Retry delays in ms: [1.2s, 1s, 1s, 2s]
+                                    const retryDelays = [1200, 1000, 1000, 2000];
+                                    if (attempt <= 4) {
+                                        setTimeout(function () { 
+                                            tryValidateUnlock(attempt + 1); 
+                                        }, retryDelays[attempt - 1]);
+                                    } else {
                                         alert('⚠️ Payment could not be verified on-chain. Please try again.');
                                     }
                                 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c434ce9 and 096e556.

📒 Files selected for processing (1)
  • assets/js/paywalled-content.js (2 hunks)
🔇 Additional comments (2)
assets/js/paywalled-content.js (2)

202-219: LGTM! Good UX improvement.

The page load scroll behavior provides a smooth experience when users return to a page with unlocked content. The implementation correctly checks for the hash, finds the target, and animates the scroll with proper offset handling.


98-197: Approve the core architectural change.

Moving the unlock logic from onClose to onSuccess is a solid improvement that eliminates the delay caused by waiting for the modal to close. The implementation correctly captures transaction data, performs the secure validation flow, and handles retries appropriately.

@xecdev
Copy link
Collaborator Author

xecdev commented Dec 6, 2025

Ready for review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
assets/js/paywalled-content.js (1)

172-176: Remove redundant console.log that fires at the wrong time.

Line 175 logs "attempt 5 Last one" immediately after scheduling the setTimeout, not when attempt 5 actually starts. Since line 128 already logs at the start of each attempt (including attempt 5 with "(final)"), this line is redundant and misleading.

                                     else if (attempt === 4) {
                                         // Worst case, one final retry after a longer delay
                                         setTimeout(function () { tryValidateUnlock(5); }, 2000); // 2s delay to give the PayButton webhook time
-                                        console.log('Retrying unlock validation (attempt 5 Last one)...');
                                     }
🧹 Nitpick comments (4)
assets/js/paywalled-content.js (4)

41-95: Consider adding error handling for AJAX calls.

The fetchUnlocked function and the nested paybutton_get_sticky_header call lack error callbacks. If these requests fail silently, the user has no indication that something went wrong after a successful payment.

         function fetchUnlocked() {
             jQuery.ajax({
                 method: 'POST',
                 url: PaywallAjax.ajaxUrl,
                 data: {
                     action: 'fetch_unlocked_content',
                     post_id: configData.postId,
                     security: PaywallAjax.nonce
                 },
                 success: function (resp) {
                     // ... existing success logic
-                }
+                },
+                error: function () {
+                    console.error('Failed to fetch unlocked content');
+                    // Optionally reload as fallback
+                    location.reload();
+                }
             });
         }

160-179: Consider refactoring retry logic to reduce repetition.

The if/else chain for retry attempts is repetitive. A data-driven approach would be more maintainable if retry counts or delays need to change again.

+                    var retryDelays = [1200, 1000, 1000, 2000]; // delays after attempts 1,2,3,4
+
                     function tryValidateUnlock(attempt) {
                         console.log('Unlock validation attempt ' + attempt + (attempt === 5 ? ' (final)' : '') + '...');
                         jQuery.post(
                             // ... existing post logic ...
                             function (resp) {
                                 if (resp && resp.success && resp.data && resp.data.unlock_token) {
                                     // ... existing success logic ...
                                 } else {
-                                    if (attempt === 1) {
-                                        setTimeout(function () { tryValidateUnlock(2); }, 1200);
-                                    }
-                                    else if(attempt === 2) {
-                                        setTimeout(function () { tryValidateUnlock(3); }, 1000);
-                                    }
-                                    else if(attempt === 3) {
-                                        setTimeout(function () { tryValidateUnlock(4); }, 1000);
-                                    }
-                                    else if (attempt === 4) {
-                                        setTimeout(function () { tryValidateUnlock(5); }, 2000);
-                                        console.log('Retrying unlock validation (attempt 5 Last one)...');
-                                    }
-                                    else {
+                                    if (attempt < 5) {
+                                        setTimeout(function () { tryValidateUnlock(attempt + 1); }, retryDelays[attempt - 1]);
+                                    } else {
                                         alert('⚠️ Payment could not be verified on-chain. Please try again.');
                                     }
                                 }
                             }
                         );
                     }

141-158: Add error handling for the mark_payment_successful call.

If this AJAX call fails after successful validation, the user receives no feedback and the unlock flow silently stops. Consider adding an error callback to inform the user or retry.

                                     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 () {
+                                            console.error('Failed to mark payment successful');
+                                            alert('⚠️ Payment verified but unlock failed. Please refresh the page.');
+                                        }
                                     });

199-216: Consider extracting the scroll logic to reduce duplication.

The scroll-to-unlocked logic appears twice (lines 64-68 and 206-213) with the same hardcoded headerOffset = 80. Extracting this to a shared helper would improve maintainability.

// At the top level or within the first document.ready
function scrollToUnlocked() {
    var $target = jQuery('#unlocked');
    if ($target.length) {
        var headerOffset = 80;
        jQuery('html, body').animate({ scrollTop: $target.offset().top - headerOffset }, 500);
    }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 096e556 and 80e004c.

📒 Files selected for processing (1)
  • assets/js/paywalled-content.js (3 hunks)
🔇 Additional comments (1)
assets/js/paywalled-content.js (1)

110-194: LGTM - Core improvement to unlock speed.

Moving the unlock flow from onClose to onSuccess with the initial delay and expanded retry logic correctly implements the PR objective. The content now unlocks immediately after successful payment rather than waiting for the modal to close.

@Klakurka
Copy link
Member

Klakurka commented Dec 6, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
assets/js/paywalled-content.js (1)

127-129: Avoid unconditional console.log in client unlock flow; gate or remove in production.

This runs in the user’s browser, and a prior reviewer already raised concerns about logging here. The current:

console.log('Unlock validation attempt ' + attempt + (attempt === 5 ? ' (final)' : '') + '...');

is useful for debugging but can clutter end-user consoles and potentially leak implementation detail.

Consider either removing this log or guarding it behind a debug flag, e.g.:

-                        console.log(
-                            'Unlock validation attempt ' + attempt +
-                            (attempt === 5 ? ' (final)' : '') + '...'
-                        );
+                        if (window.PaywallDebug) {
+                            console.log(
+                                'Unlock validation attempt ' + attempt +
+                                (attempt === 5 ? ' (final)' : '') + '...'
+                            );
+                        }

This keeps the diagnostics available during development while keeping production output clean.

🧹 Nitpick comments (2)
assets/js/paywalled-content.js (2)

9-21: Update comments to match the new onSuccess-based unlock flow (no full-page reload).

The file header (lines 9–21) and the inline comment at Line 37 still describe the old behavior (unlock handled via onSuccessmark_payment_successful → page reload, and shared state “consumed in onClose”). The actual behavior is now:

  • onSuccess captures tx data.
  • A delayed tryValidateUnlock(1) sequence performs validate_unlock_txmark_payment_successfulfetchUnlocked().
  • The page is not reloaded; content and header are updated in-place, and the URL hash is adjusted.

To avoid confusion for future maintainers, it would be good to align the comments with the current flow and remove references to onClose. For example:

- *   - Setting up an onSuccess callback that triggers when a payment is completed.
- *
- * How it works:
- *   1. On document ready, jQuery selects all ".paybutton-container" elements.
- *   2. For each container:
- *      - The script retrieves its configuration data via the data-config attribute.
- *      - If the data is a JSON string, it parses it into a JavaScript object.
- *      - It then sets up an onSuccess callback. When the payment is successful, this callback:
- *           a) Sends an AJAX POST request (using jQuery's $.ajax) to the server endpoint 
- *              (action: "mark_payment_successful") with the transaction details.
- *           b) On success, waits 2000 ms (2 seconds) and then reloads the page so that
- *              the now unlocked content is displayed.
- *   3. Finally, the render() method is called on the container to display the button.
+ *   - Setting up an onSuccess callback that triggers when a payment is completed.
+ *
+ * How it works:
+ *   1. On document ready, jQuery selects all ".paybutton-container" elements.
+ *   2. For each container:
+ *      - The script retrieves its configuration data via the data-config attribute.
+ *      - If the data is a JSON string, it parses it into a JavaScript object.
+ *      - It then sets up an onSuccess callback. When the payment is successful, this callback:
+ *           a) Captures the transaction details from the PayButton tx object.
+ *           b) After a short delay, starts a retry loop that calls "validate_unlock_tx" on the server.
+ *           c) When validation returns an unlock token, sends "mark_payment_successful" with the tx details.
+ *           d) On success, calls fetchUnlocked() to replace the paywalled segment with unlocked content,
+ *              update the URL hash to "#unlocked", and refresh the sticky header.
+ *   3. Finally, the render() method is called on the container to display the button.
@@
-        // Shared state: user wallet address + unlock tx captured in onSuccess, consumed in onClose.
+        // Shared state: user wallet address + unlock tx captured in onSuccess; copies are passed into
+        // the unlock validation flow and then this shared state is cleared.
@@
-        // Configure the PayButton like before, but:
-        // - onSuccess captures tx data and does the secure validate -> mark_payment_successful -> fetch flow
+        // Configure the PayButton like before, but:
+        // - onSuccess now captures tx data and runs the secure
+        //   validate_unlock_tx -> mark_payment_successful -> fetchUnlocked flow

Also applies to: 37-40, 97-99


160-187: Retry/backoff logic is correct but can be simplified and made easier to tune.

Functionally, the chained retries (attempts 1→5 with the specified delays, plus a final alert) look correct and match the described behavior. The nested if / else if ladder, however, is a bit repetitive and harder to tweak later.

You can keep the same timing semantics while collapsing this into a map-based lookup:

-                                } else {
-                                    if (attempt === 1) {
-                                        // Retry after brief delay
-                                        setTimeout(function () { tryValidateUnlock(2); }, 1200); // 1.2s delay to give the PayButton webhook time
-                                    }
-                                    else if(attempt === 2) {
-                                        // Retry after brief delay
-                                        setTimeout(function () { tryValidateUnlock(3); }, 1000); // 1s delay to give the PayButton webhook time
-                                    }
-                                    else if(attempt === 3) {
-                                        // Retry after brief delay
-                                        setTimeout(function () { tryValidateUnlock(4); }, 1000); // 1s delay to give the PayButton webhook time
-                                    }
-                                    else if (attempt === 4) {
-                                        // Worst case, one final retry after a longer delay
-                                        setTimeout(function () { tryValidateUnlock(5); }, 2000); // 2s delay to give the PayButton webhook time
-                                    }
-                                    else {
-                                        alert('⚠️ Payment could not be verified on-chain. Please try again.');
-                                    }
-                                }
+                                } else {
+                                    // Map attempt -> delay before scheduling the next attempt
+                                    var retryDelaysMs = {
+                                        1: 1200, // attempt 1 -> schedule attempt 2 after 1.2s
+                                        2: 1000, // attempt 2 -> schedule attempt 3 after 1s
+                                        3: 1000, // attempt 3 -> schedule attempt 4 after 1s
+                                        4: 2000  // attempt 4 -> schedule attempt 5 after 2s
+                                    };
+
+                                    var delay = retryDelaysMs[attempt];
+
+                                    if (delay) {
+                                        setTimeout(function () {
+                                            tryValidateUnlock(attempt + 1);
+                                        }, delay);
+                                    } else {
+                                        alert('⚠️ Payment could not be verified on-chain. Please try again.');
+                                    }
+                                }

Optionally, you could also attach a .fail handler on the jQuery.post for validate_unlock_tx and route it through the same retry/alert path so that network errors don’t silently stall the flow.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80e004c and 7054650.

📒 Files selected for processing (1)
  • assets/js/paywalled-content.js (3 hunks)

@xecdev xecdev requested a review from Klakurka December 6, 2025 09:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
assets/js/paywalled-content.js (1)

139-184: Network errors bypass retry logic.

The jQuery.post for validation only handles the success callback. If the network request fails (timeout, server error, etc.), neither the success nor any error handler fires, leaving the user waiting indefinitely without triggering a retry.

Consider adding an error handler that treats network failures as retryable:

                     jQuery.post(
                         PaywallAjax.ajaxUrl,
                         {
                             action: 'validate_unlock_tx',
                             security: PaywallAjax.nonce,
                             wallet_address: addrCopy,
                             tx_hash: hashCopy,
                             post_id: postIdCopy
                         },
                         function (resp) {
                             // ... existing success logic ...
                         }
-                    );
+                    ).fail(function () {
+                        if (attempt < maxAttempts) {
+                            const nextAttempt = attempt + 1;
+                            const nextDelay = baseDelayMs + (nextAttempt - 1) * stepDelayMs;
+                            setTimeout(function () {
+                                tryValidateUnlock(nextAttempt);
+                            }, nextDelay);
+                        } else {
+                            alert('⚠️ Payment could not be verified. Please try again.');
+                        }
+                    });
🧹 Nitpick comments (2)
assets/js/paywalled-content.js (2)

42-95: Add error handling for AJAX calls.

The fetchUnlocked() function makes two AJAX requests but neither has an error callback. If the server is unreachable or returns a 5xx error, the user sees no feedback and the content remains locked without explanation.

Consider adding error handling at minimum for the primary content fetch:

         function fetchUnlocked() {
             jQuery.ajax({
                 method: 'POST',
                 url: PaywallAjax.ajaxUrl,
                 data: {
                     action: 'fetch_unlocked_content',
                     post_id: configData.postId,
                     security: PaywallAjax.nonce
                 },
                 success: function (resp) {
                     // ... existing success logic ...
-                }
+                },
+                error: function () {
+                    alert('⚠️ Failed to load unlocked content. Please refresh the page.');
+                }
             });
         }

132-137: Consider removing commented-out debug code.

The commented-out console.log addresses the concern about not logging in production. However, leaving dead code can clutter the codebase. Consider either:

  • Removing it entirely if not needed
  • Using a debug flag: if (PaywallAjax.debug) { console.log(...); }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7054650 and 242e337.

📒 Files selected for processing (1)
  • assets/js/paywalled-content.js (4 hunks)
🔇 Additional comments (2)
assets/js/paywalled-content.js (2)

170-178: LGTM: Clean retry implementation with incremental backoff.

The refactored retry logic is much cleaner than the previous hardcoded attempts. The incremental backoff (1s → 1.5s → 2s → 2.5s) follows Klakurka's suggestion and is easy to maintain. Based on past review comments.


201-218: LGTM: Scroll behavior on page load.

This correctly handles the page load/refresh scenario where #unlocked is already in the URL, complementing the in-page scroll in fetchUnlocked().

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
assets/js/paywalled-content.js (2)

37-39: Update outdated comment referencing removed callback.

The comment mentions "consumed in onClose," but the onClose callback was removed in this PR. The state is now consumed within the onSuccess callback flow.

Apply this diff:

-        // Shared state: user wallet address + unlock tx captured in onSuccess, consumed in onClose.
+        // Shared state: user wallet address + unlock tx captured and consumed in onSuccess.

132-177: Add error handling for AJAX failures.

The jQuery.post and jQuery.ajax calls lack error handlers. If a network error or server error occurs (distinct from a logical validation failure), the user receives no feedback and may be left in limbo—especially problematic if validate_unlock_tx succeeds but mark_payment_successful or fetchUnlocked fail.

Add error handlers to provide user feedback on transport failures:

                     function tryValidateUnlock(attempt) {
                         const maxAttempts  = 4;      // total attempts
                         const baseDelayMs  = 1000;   // 1.0s
                         const stepDelayMs  = 500;    // +0.5s per retry
 
                         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();
-                                        }
+                                        },
+                                        error: function () {
+                                            alert('⚠️ Failed to mark payment. Please refresh the page or contact support.');
+                                        }
                                     });
                                 } else {
                                     if (attempt < maxAttempts) {
                                         // Retry after brief delay with incremental backoff:
                                         // 1s, 1.5s, 2s, 2.5s
                                         const nextAttempt = attempt + 1;
                                         const nextDelay   = baseDelayMs + (nextAttempt - 1) * stepDelayMs;
 
                                         setTimeout(function () {
                                             tryValidateUnlock(nextAttempt);
                                         }, nextDelay);
                                     } else {
                                         alert('⚠️ Payment could not be verified on-chain. Please try again.');
                                     }
                                 }
-                            }
+                            },
+                            error: function () {
+                                // Network/server error during validation - treat as retriable
+                                if (attempt < maxAttempts) {
+                                    const nextAttempt = attempt + 1;
+                                    const nextDelay   = baseDelayMs + (nextAttempt - 1) * stepDelayMs;
+                                    setTimeout(function () {
+                                        tryValidateUnlock(nextAttempt);
+                                    }, nextDelay);
+                                } else {
+                                    alert('⚠️ Network error during validation. Please refresh the page or contact support.');
+                                }
+                            }
                         );
                     }

Similarly, add an error handler to fetchUnlocked (lines 42-95):

success: function (resp) {
    // ... existing success logic ...
},
error: function () {
    alert('⚠️ Failed to load unlocked content. Please refresh the page.');
}
🧹 Nitpick comments (1)
assets/js/paywalled-content.js (1)

194-211: Page-load scrolling works correctly.

The scroll-to-unlocked behavior on page load is correctly implemented and necessary for users who return to or refresh an already-unlocked page. The logic mirrors the scroll behavior in fetchUnlocked (lines 63-69).

Optionally, the scroll logic could be extracted into a shared helper to reduce duplication:

function scrollToUnlocked() {
    var $target = jQuery('#unlocked');
    if ($target.length) {
        var headerOffset = 80;
        jQuery('html, body').animate({ scrollTop: $target.offset().top - headerOffset }, 500);
    }
}

Then call it from both locations. However, this is a minor optimization and not critical.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 242e337 and d350271.

📒 Files selected for processing (1)
  • assets/js/paywalled-content.js (4 hunks)
🔇 Additional comments (2)
assets/js/paywalled-content.js (2)

163-171: Clean implementation of incremental backoff.

The loop-based retry logic with computed delays is much cleaner than the previous if-else chain and correctly implements the requested backoff strategy (1.5s, 2s, 2.5s).


180-183: Initial delay is appropriate.

The 1000ms delay before the first validation attempt gives the PayButton webhook reasonable time to process the transaction.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
assets/js/paywalled-content.js (1)

110-189: Note: PR description mentions console logging, but logs were removed.

The PR description states "added console logging for retry attempts," but based on past review feedback from Klakurka, the console.log statements were removed (as confirmed by xecdev's comment). The PR description should be updated to reflect this change.

🧹 Nitpick comments (1)
assets/js/paywalled-content.js (1)

194-211: Extract duplicate scroll logic into a shared function.

The scrolling logic here duplicates lines 63-69 in the fetchUnlocked function (same headerOffset, $target selection, and animation). Both code paths are needed—one for dynamic unlock, one for page load—but extracting the shared logic into a helper function would improve maintainability.

Consider extracting the scroll behavior:

function scrollToUnlocked() {
    var $target = jQuery('#unlocked');
    if ($target.length) {
        var headerOffset = 80;
        jQuery('html, body').animate({ 
            scrollTop: $target.offset().top - headerOffset 
        }, 500);
    }
}

Then call it from both locations:

  • In fetchUnlocked (replace lines 64-68)
  • In the page-load handler (replace lines 199-209)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d350271 and 77a12b8.

📒 Files selected for processing (1)
  • assets/js/paywalled-content.js (5 hunks)
🔇 Additional comments (3)
assets/js/paywalled-content.js (3)

37-39: LGTM! State variables properly scoped.

The shared state variables are correctly declared within the closure and cleared after being copied for the validation flow.


128-131: LGTM! Retry configuration matches review feedback.

The retry logic correctly implements Klakurka's suggestion for a loop with incremental backoff (1s, 1.5s, 2s, 2.5s delays between attempts).


163-171: LGTM! Retry backoff logic is correct.

The incremental backoff calculation correctly produces delays of 1s, 1.5s, 2s, and 2.5s between attempts, matching the inline comment and the intended behavior.

@xecdev
Copy link
Collaborator Author

xecdev commented Dec 7, 2025

Ready for review @Klakurka. Applied your feedback.

@xecdev xecdev linked an issue Dec 7, 2025 that may be closed by this pull request
@Klakurka Klakurka merged commit 3ab169a into master Dec 7, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement (UI/UX/feature) New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve unlock speed

2 participants