Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ad56416
Create and register paywall config
av3nger Apr 21, 2026
28c461e
Render function + three layout templates
av3nger Apr 21, 2026
df0ee89
Prepare render for paywal config
av3nger Apr 21, 2026
d335a33
Add builder UI
av3nger Apr 21, 2026
dc8d456
Fix phpcs warnings
av3nger Apr 21, 2026
a403643
Boilerplate paywal preview
av3nger Apr 22, 2026
28a9230
Drop custom CSS field from the builder
av3nger Apr 22, 2026
e8a69d8
Adjust UI/UX
av3nger Apr 22, 2026
03db76c
Save work in progress
av3nger Apr 22, 2026
e978111
Adjust styles
av3nger Apr 23, 2026
03978ab
Fix flagged issues
av3nger Apr 26, 2026
b8e2df8
Fix issues
av3nger Apr 26, 2026
70af3b5
Improve post edit screen
av3nger Apr 28, 2026
3b40da0
Fix code audit issues
av3nger May 4, 2026
e2a8617
Fix flagged issues
av3nger May 4, 2026
e50f529
Fix FOC issue
av3nger May 5, 2026
cfef9f6
Improve paywall logic
av3nger May 5, 2026
4c5ec31
Improve paywall banner theme compatibility
av3nger May 5, 2026
d1740cf
Fix another round of flagged issues
av3nger May 5, 2026
0e59044
Fix save button
av3nger May 6, 2026
01df5b4
Drop banner layout
av3nger May 13, 2026
efe0fb1
Adjust layout, removing heading selector
av3nger May 13, 2026
ad9e596
Itemised benefits functionality
av3nger May 13, 2026
45faa2c
Minor style adjustments
av3nger May 13, 2026
16f45d8
Adjust accent and background colors functionality
av3nger May 13, 2026
66ede34
Adjust advanced settings
av3nger May 13, 2026
1c29158
Adjust layout to be closer to the prototype
av3nger May 13, 2026
3ad49f9
Final adjustments
av3nger May 13, 2026
ea5542b
Fix paywal preview
av3nger May 21, 2026
7eca306
Refactor the color selectors
av3nger May 21, 2026
f9f1839
Latest changes from ticket requests
av3nger May 21, 2026
72122d0
Fix scrolling issues
av3nger May 21, 2026
e6f32c1
Fix preview
av3nger May 24, 2026
971da5f
Merge remote-tracking branch 'origin/main' into feature/MR-31-paywall
av3nger Jun 19, 2026
77151a6
Fix issues, make improvements
av3nger Jun 19, 2026
a249ed5
UI/UX adjustments based on Jira feedback
av3nger Jun 22, 2026
65e9fe5
Changes based on feedback from Jira
av3nger Jun 30, 2026
7973d05
Minor cleanup
av3nger Jun 30, 2026
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
6 changes: 4 additions & 2 deletions wordpress/wp-content/plugins/memberful-wp/js/src/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,17 @@ jQuery(document).ready(function($){
let editor = tinyMCE.editors[0];
let globalContent=$('#use_global_marketing_checkbox');
let snippetContent=$('#use_global_snippets_checkbox');
let modeRadios=$('input[name="memberful_paywall[mode]"]');

function checkGlobalValidity(e){
let isGlobal=globalContent.is(':checked');
let isSnippets=snippetContent.is(':checked');
let isCustomHtml=modeRadios.filter(':checked').val() === 'custom_html';
let submit=$('button[type="submit"]');
let isContentEmpty=!editor.getContent().trim();

let warning=$('#global_content_required');

if( isGlobal && isContentEmpty ){
if( isGlobal && isCustomHtml && isContentEmpty ){
submit.prop('disabled', true);
warning.show();

Expand All @@ -86,6 +87,7 @@ jQuery(document).ready(function($){

globalContent.change(checkGlobalValidity)
snippetContent.change(checkGlobalValidity)
modeRadios.change(checkGlobalValidity)
editor.on('change', checkGlobalValidity);
}

Expand Down
189 changes: 189 additions & 0 deletions wordpress/wp-content/plugins/memberful-wp/js/src/paywall-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
jQuery(function ($) {
const $form = $('.memberful-paywall-builder__panel[data-panel="builder"]');
const $modeInputs = $('input[name="memberful_paywall[mode]"]');
const $panels = $('.memberful-paywall-builder__panel');
const $preview = $('#memberful-paywall-preview');

const preview = window.memberfulPaywallPreview || {};
const DEBOUNCE_MS = 250;

let debounceTimer = null;
let requestSeq = 0;

function setColorFieldValue($field, color) {
const normalized = color ? color.toLowerCase() : '';
const $presetSwatches = $field.find('.memberful-paywall-builder__swatch[data-color]');
const $customSwatch = $field.find('.memberful-paywall-builder__swatch--custom');
let presetSelected = false;

$field.find('.memberful-paywall-builder__color-input').val(normalized);

$presetSwatches.each(function () {
const $swatch = $(this);
const isSelected = normalized !== '' && ($swatch.attr('data-color') || '').toLowerCase() === normalized;
$swatch.toggleClass('is-selected', isSelected).attr('aria-pressed', isSelected ? 'true' : 'false');
presetSelected = presetSelected || isSelected;
});

$customSwatch.toggleClass('is-selected', normalized !== '' && !presetSelected);
if (normalized !== '' && !presetSelected) {
$customSwatch.css('--memberful-custom-swatch-color', normalized);
} else {
$customSwatch.css('--memberful-custom-swatch-color', '');
}

$field.find('.memberful-paywall-builder__color-reset').prop('disabled', normalized === '');
}

$('[data-color-field]').each(function () {
const $field = $(this);
setColorFieldValue($field, $field.find('.memberful-paywall-builder__color-input').val() || '');
});

function applyMode(mode) {
$panels.each(function () {
this.style.display = this.dataset.panel === mode ? '' : 'none';
});
}

$modeInputs.on('change', function () {
if (!this.checked) {
return;
}

applyMode(this.value);

if (this.value === 'builder') {
refreshPreview();
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

applyMode($modeInputs.filter(':checked').val() || 'builder');

function collectConfig() {
return {
mode: $('input[name="memberful_paywall[mode]"]:checked').val() || 'builder',
layout: $('input[name="memberful_paywall[layout]"]:checked').val() || 'card',
heading: $('#memberful-paywall-heading').val() || '',
subheading: $('#memberful-paywall-subheading').val() || '',
features: $('#memberful-paywall-benefits .memberful-paywall-builder__benefit-input').map(function () {
return $(this).val();
}).get(),
button_label: $('#memberful-paywall-button-label').val() || '',
subscribe_url: $('#memberful-paywall-subscribe-url').val() || '',
sign_in_url: $('#memberful-paywall-signin-url').val() || '',
brand_color: $('#memberful-paywall-brand-color').val() || '',
background_color: $('#memberful-paywall-background-color').val() || '',
button_shape: $('input[name="memberful_paywall[button_shape]"]:checked').val() || 'rounded',
};
}

function refreshPreview() {
if (!preview.ajaxUrl || !preview.action || !preview.nonce || !$preview.length) {
return;
}

clearTimeout(debounceTimer);

const seq = ++requestSeq;
const body = new URLSearchParams();
body.set('action', preview.action);
body.set('nonce', preview.nonce);

const config = collectConfig();
Object.keys(config).forEach(function (key) {
const value = config[key];
if (Array.isArray(value)) {
value.forEach(function (item) {
body.append('config[' + key + '][]', item);
});
} else {
body.append('config[' + key + ']', value);
}
});

fetch(preview.ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString(),
})
.then(function (res) {
if (!res.ok) {
throw new Error('Request failed: ' + res.status);
}

return res.json();
})
.then(function (json) {
if (seq !== requestSeq) {
return;
}

if (json && json.success && json.data && json.data.html) {
$preview.attr('srcdoc', json.data.html);
}
})
.catch(function (err) {
console.error('Paywall preview failed', err);
});
}

function scheduleRefresh() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(refreshPreview, DEBOUNCE_MS);
}

$form.on('input', 'input[type="text"], input[type="url"], textarea', scheduleRefresh);
$form.on('change', 'input[type="radio"], select', refreshPreview);

$form.on('click', '.memberful-paywall-builder__swatch[data-color]', function (e) {
e.preventDefault();

setColorFieldValue($(this).closest('[data-color-field]'), $(this).attr('data-color') || '');
refreshPreview();
});

$form.on('input change', '.memberful-paywall-builder__swatch--custom input[type="color"]', function () {
setColorFieldValue($(this).closest('[data-color-field]'), this.value || '');
scheduleRefresh();
});

$form.on('click', '.memberful-paywall-builder__color-reset', function (e) {
e.preventDefault();

setColorFieldValue($(this).closest('[data-color-field]'), '');
refreshPreview();
});

$form.on('click', '.memberful-paywall-builder__benefit-add', function (e) {
e.preventDefault();

const template = document.getElementById('memberful-paywall-benefit-template');
const list = document.getElementById('memberful-paywall-benefits');
if (!template || !list) {
return;
}

list.appendChild(template.content.cloneNode(true));
$(list).find('.memberful-paywall-builder__benefit-input').last().trigger('focus');
refreshPreview();
});

$form.on('click', '.memberful-paywall-builder__benefit-remove', function (e) {
e.preventDefault();
$(this).closest('.memberful-paywall-builder__benefit').remove();
refreshPreview();
});

const $headingInput = $('#memberful-paywall-heading');
const $headingCounter = $('[data-counter-for="memberful-paywall-heading"]');
const headingMax = parseInt($headingCounter.attr('data-max'), 10) || 60;
$headingInput.on('input', function () {
$headingCounter.text(this.value.length + '/' + headingMax);
});

if (($modeInputs.filter(':checked').val() || 'builder') === 'builder') {
refreshPreview();
}
});
1 change: 1 addition & 0 deletions wordpress/wp-content/plugins/memberful-wp/memberful-wp.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
require_once MEMBERFUL_DIR . '/src/widgets.php';
require_once MEMBERFUL_DIR . '/src/endpoints.php';
require_once MEMBERFUL_DIR . '/src/marketing_content.php';
require_once MEMBERFUL_DIR . '/src/paywall.php';
require_once MEMBERFUL_DIR . '/src/content_filter.php';
require_once MEMBERFUL_DIR . '/src/search_filter.php';
require_once MEMBERFUL_DIR . '/src/entities.php';
Expand Down
56 changes: 49 additions & 7 deletions wordpress/wp-content/plugins/memberful-wp/src/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,14 @@ function memberful_wp_menu() {

function memberful_wp_admin_enqueue_scripts() {
$screen = get_current_screen();
$admin_css_path = MEMBERFUL_DIR . '/stylesheets/admin.css';

if ( strpos( 'memberful', $screen->id ) !== null ) {
wp_enqueue_style(
'memberful-admin',
plugins_url( 'stylesheets/admin.css' , dirname(__FILE__) )
plugins_url( 'stylesheets/admin.css' , dirname(__FILE__) ),
array(),
file_exists( $admin_css_path ) ? filemtime( $admin_css_path ) : MEMBERFUL_VERSION
);
wp_enqueue_script(
'memberful-admin',
Expand All @@ -127,6 +130,35 @@ function memberful_wp_admin_enqueue_scripts() {
);
}

if (
'memberful_options' === filter_input( INPUT_GET, 'page' )
&& 'global_marketing' === filter_input( INPUT_GET, 'subpage' )
) {
$paywall_css_path = MEMBERFUL_DIR . '/stylesheets/paywall.css';
$paywall_builder_path = MEMBERFUL_DIR . '/js/build/paywall-builder.js';

wp_enqueue_style(
'memberful-paywall',
MEMBERFUL_URL . '/stylesheets/paywall.css',
array(),
file_exists( $paywall_css_path ) ? filemtime( $paywall_css_path ) : MEMBERFUL_VERSION
);

wp_enqueue_script(
'memberful-paywall-builder',
MEMBERFUL_URL . '/js/build/paywall-builder.js',
array('jquery'),
file_exists( $paywall_builder_path ) ? filemtime( $paywall_builder_path ) : MEMBERFUL_VERSION,
true
);

wp_localize_script(
'memberful-paywall-builder',
'memberfulPaywallPreview',
Memberful_Paywall_Preview::script_args()
);
}

wp_enqueue_script(
'memberful-menu',
plugins_url( 'js/src/menu.js', dirname( __FILE__ ) ),
Expand Down Expand Up @@ -697,8 +729,17 @@ function memberful_wp_global_marketing() {
if ( isset( $_POST['memberful_use_global_marketing'] ) ) {
update_option( 'memberful_use_global_marketing', true );
update_option( 'memberful_global_marketing_override', filter_input( INPUT_POST, 'memberful_global_marketing_override', FILTER_SANITIZE_NUMBER_INT ) );
update_option( 'memberful_global_marketing_content', memberful_wp_kses_post( filter_input( INPUT_POST, 'memberful_global_marketing_content' ) ) );
update_option( 'memberful_use_global_snippets', (int) isset($_POST['memberful_use_global_snippets']));
update_option( 'memberful_use_global_snippets', (int) isset( $_POST['memberful_use_global_snippets'] ) );

$paywall_input = filter_input( INPUT_POST, 'memberful_paywall', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
if ( is_array( $paywall_input ) && ! empty( $paywall_input ) ) {
Memberful_Paywall_Config::save( $paywall_input );
}

$config_mode = ( is_array( $paywall_input ) && isset( $paywall_input['mode'] ) && in_array( $paywall_input['mode'], Memberful_Paywall_Config::MODES, true ) ) ? $paywall_input['mode'] : 'builder';
if ( 'custom_html' === $config_mode ) {
update_option( 'memberful_global_marketing_content', memberful_wp_kses_post( (string) filter_input( INPUT_POST, 'memberful_global_marketing_content' ) ) );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Custom HTML save mode fallback

Medium Severity

When the posted memberful_paywall payload is missing or lacks a valid mode, save logic assumes builder and skips updating memberful_global_marketing_content, so Custom HTML edits can be discarded even when the site is configured for that mode.

Fix in Cursor Fix in Web

Triggered by team rule: Code Review Guidelines

Reviewed by Cursor Bugbot for commit 65e9fe5. Configure here.

}
} else {
update_option( 'memberful_use_global_marketing', false );
}
Expand All @@ -712,11 +753,12 @@ function memberful_wp_global_marketing() {
memberful_wp_render(
'global_marketing',
array(
'use_global_marketing' => $use_global_marketing,
'use_global_snippets' => $use_global_snippets,
'global_marketing_content' => $global_marketing_content,
'use_global_marketing' => $use_global_marketing,
'use_global_snippets' => $use_global_snippets,
'global_marketing_content' => $global_marketing_content,
'global_marketing_override' => $global_marketing_override,
'form_target' => memberful_wp_plugin_global_marketing_url()
'paywall_config' => Memberful_Paywall_Config::get(),
'form_target' => memberful_wp_plugin_global_marketing_url(),
)
);
}
Expand Down
27 changes: 21 additions & 6 deletions wordpress/wp-content/plugins/memberful-wp/src/global_marketing.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,33 @@
*/
function memberful_get_global_replacement($marketing_content){
$override = get_option( 'memberful_global_marketing_override' );
$global_marketing_content = get_option( 'memberful_global_marketing_content' );

if($override) {
return $global_marketing_content;
if ( $override ) {
return memberful_wp_resolve_global_marketing_content();
}

if(empty(trim($marketing_content))){
return $global_marketing_content;
if ( empty( trim( $marketing_content ) ) ) {
return memberful_wp_resolve_global_marketing_content();
}

return $marketing_content;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Resolve the global marketing HTML from whichever source the paywall config points to.
*
* @return string
*/
function memberful_wp_resolve_global_marketing_content(): string {
$config = Memberful_Paywall_Config::get();

if ( 'builder' === $config['mode'] ) {
return Memberful_Paywall_Renderer::render( $config );
}

return (string) get_option( 'memberful_global_marketing_content' );
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Filter the paywall to return a "teaser".
*
Expand Down Expand Up @@ -83,7 +97,8 @@ function memberful_apply_global_snippets_content_filter( $memberful_marketing_co
}
}

$wrapped_teaser = "<div class='memberful-global-teaser-content'>$teaser</div>";
$teaser_class = apply_filters( 'memberful_global_teaser_class', 'memberful-global-teaser-content' );
$wrapped_teaser = "<div class='" . esc_attr( $teaser_class ) . "'>$teaser</div>";

if ( $has_teaser && ! did_filter( 'memberful_teaser_css' ) ) {
$wrapped_teaser .= apply_filters( 'memberful_teaser_css', memberful_get_teaser_css() );
Expand Down
7 changes: 7 additions & 0 deletions wordpress/wp-content/plugins/memberful-wp/src/metabox.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ function memberful_wp_metabox_types() {
return apply_filters( 'memberful_metabox_post_types', $types );
}

function memberful_global_marketing_overrides_post_content() {
return get_option( 'memberful_use_global_marketing' )
&& get_option( 'memberful_global_marketing_override' );
}

function memberful_wp_add_metabox() {
if ( ! get_option('memberful_site', FALSE) )
return;
Expand Down Expand Up @@ -54,6 +59,7 @@ function memberful_wp_metabox( $post ) {
$view_vars['marketing_content'] = reset($marketing_content);
$view_vars['viewable_by_any_registered_users'] = memberful_wp_get_post_available_to_any_registered_users( $post->ID );
$view_vars['viewable_by_anybody_subscribed_to_a_plan'] = memberful_wp_get_post_available_to_anybody_subscribed_to_a_plan( $post->ID );
$view_vars['global_marketing_overrides_post_content'] = memberful_global_marketing_overrides_post_content();

memberful_wp_render( 'metabox', $view_vars );
}
Expand Down Expand Up @@ -154,6 +160,7 @@ function memberful_wp_add_term_metabox( $term ) {
$view_vars['marketing_content'] = reset($marketing_content);
$view_vars['viewable_by_any_registered_users'] = memberful_wp_is_term_available_to_any_registered_users( $term->term_id );
$view_vars['viewable_by_anybody_subscribed_to_a_plan'] = memberful_wp_is_term_available_to_anybody_subscribed_to_a_plan( $term->term_id );
$view_vars['global_marketing_overrides_post_content'] = memberful_global_marketing_overrides_post_content();

memberful_wp_render( 'metabox', $view_vars );
}
Expand Down
Loading