diff --git a/.gitignore b/.gitignore index c0b8df13..cf79054b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.idea # Manage wordpress/wp-content/plugins/memberful-wp and ignore everything else # from the wordpress/ folder. diff --git a/wordpress/wp-content/plugins/memberful-wp/js/src/admin.js b/wordpress/wp-content/plugins/memberful-wp/js/src/admin.js index 1bc1984a..50a6a589 100755 --- a/wordpress/wp-content/plugins/memberful-wp/js/src/admin.js +++ b/wordpress/wp-content/plugins/memberful-wp/js/src/admin.js @@ -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(); @@ -86,6 +87,7 @@ jQuery(document).ready(function($){ globalContent.change(checkGlobalValidity) snippetContent.change(checkGlobalValidity) + modeRadios.change(checkGlobalValidity) editor.on('change', checkGlobalValidity); } diff --git a/wordpress/wp-content/plugins/memberful-wp/js/src/paywall-builder.js b/wordpress/wp-content/plugins/memberful-wp/js/src/paywall-builder.js new file mode 100644 index 00000000..03230a33 --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/js/src/paywall-builder.js @@ -0,0 +1,167 @@ +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 $colorInputs = $('.memberful-paywall-builder__color'); + + const preview = window.memberfulPaywallPreview || {}; + const DEBOUNCE_MS = 250; + + let debounceTimer = null; + let requestSeq = 0; + + $colorInputs.each(function () { + const $input = $(this); + let palettes = true; + const palettesAttr = $input.attr('data-palettes'); + if (palettesAttr) { + try { + const parsed = JSON.parse(palettesAttr); + if (Array.isArray(parsed) && parsed.length) { + palettes = parsed; + } + } catch (e) { + // Fall back to wpColorPicker's default palette. + } + } + + $input.wpColorPicker({ + palettes: palettes, + change: function () { + setTimeout(scheduleRefresh, 0); + }, + clear: function () { + setTimeout(scheduleRefresh, 0); + }, + }); + }); + + 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(); + } + }); + + 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__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(); + } +}); diff --git a/wordpress/wp-content/plugins/memberful-wp/memberful-wp.php b/wordpress/wp-content/plugins/memberful-wp/memberful-wp.php index 5ba9d8db..daeff845 100755 --- a/wordpress/wp-content/plugins/memberful-wp/memberful-wp.php +++ b/wordpress/wp-content/plugins/memberful-wp/memberful-wp.php @@ -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'; diff --git a/wordpress/wp-content/plugins/memberful-wp/src/admin.php b/wordpress/wp-content/plugins/memberful-wp/src/admin.php index fa18a503..f0d5baf1 100755 --- a/wordpress/wp-content/plugins/memberful-wp/src/admin.php +++ b/wordpress/wp-content/plugins/memberful-wp/src/admin.php @@ -127,6 +127,34 @@ function memberful_wp_admin_enqueue_scripts() { ); } + if ( + 'memberful_options' === filter_input( INPUT_GET, 'page' ) + && 'global_marketing' === filter_input( INPUT_GET, 'subpage' ) + ) { + wp_enqueue_style( 'wp-color-picker' ); + + wp_enqueue_style( + 'memberful-paywall', + MEMBERFUL_URL . '/stylesheets/paywall.css', + array(), + MEMBERFUL_VERSION + ); + + wp_enqueue_script( + 'memberful-paywall-builder', + MEMBERFUL_URL . '/js/build/paywall-builder.js', + array( 'jquery', 'wp-color-picker' ), + 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__ ) ), @@ -691,8 +719,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' ) ) ); + } } else { update_option( 'memberful_use_global_marketing', false ); } @@ -706,11 +743,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(), ) ); } diff --git a/wordpress/wp-content/plugins/memberful-wp/src/global_marketing.php b/wordpress/wp-content/plugins/memberful-wp/src/global_marketing.php index ed1598c9..c3a7eb6c 100644 --- a/wordpress/wp-content/plugins/memberful-wp/src/global_marketing.php +++ b/wordpress/wp-content/plugins/memberful-wp/src/global_marketing.php @@ -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; } +/** + * 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' ); +} + /** * Filter the paywall to return a "teaser". * @@ -83,7 +97,8 @@ function memberful_apply_global_snippets_content_filter( $memberful_marketing_co } } - $wrapped_teaser = "
"; + $teaser_class = apply_filters( 'memberful_global_teaser_class', 'memberful-global-teaser-content' ); + $wrapped_teaser = ""; if ( $has_teaser && ! did_filter( 'memberful_teaser_css' ) ) { $wrapped_teaser .= apply_filters( 'memberful_teaser_css', memberful_get_teaser_css() ); diff --git a/wordpress/wp-content/plugins/memberful-wp/src/metabox.php b/wordpress/wp-content/plugins/memberful-wp/src/metabox.php index d82b7ac3..27908fb8 100755 --- a/wordpress/wp-content/plugins/memberful-wp/src/metabox.php +++ b/wordpress/wp-content/plugins/memberful-wp/src/metabox.php @@ -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; @@ -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 ); } @@ -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 ); } diff --git a/wordpress/wp-content/plugins/memberful-wp/src/options.php b/wordpress/wp-content/plugins/memberful-wp/src/options.php index 0f16f406..fd0351be 100755 --- a/wordpress/wp-content/plugins/memberful-wp/src/options.php +++ b/wordpress/wp-content/plugins/memberful-wp/src/options.php @@ -1,34 +1,35 @@ NULL, - 'memberful_client_secret' => NULL, - 'memberful_site' => NULL, - 'memberful_custom_domain' => NULL, - 'memberful_api_key' => NULL, - 'memberful_webhook_secret' => NULL, - 'memberful_products' => array(), - 'memberful_subscriptions' => array(), - 'memberful_acl' => array(), - 'memberful_embed_enabled' => FALSE, - 'memberful_error_log' => array(), - 'memberful_role_active_customer' => 'subscriber', - 'memberful_role_inactive_customer' => 'subscriber', - 'memberful_plan_role_mappings' => array(), - 'memberful_use_per_plan_roles' => FALSE, + 'memberful_client_id' => NULL, + 'memberful_client_secret' => NULL, + 'memberful_site' => NULL, + 'memberful_custom_domain' => NULL, + 'memberful_api_key' => NULL, + 'memberful_webhook_secret' => NULL, + 'memberful_products' => array(), + 'memberful_subscriptions' => array(), + 'memberful_acl' => array(), + 'memberful_embed_enabled' => FALSE, + 'memberful_error_log' => array(), + 'memberful_role_active_customer' => 'subscriber', + 'memberful_role_inactive_customer' => 'subscriber', + 'memberful_plan_role_mappings' => array(), + 'memberful_use_per_plan_roles' => FALSE, 'memberful_posts_available_to_any_registered_user' => array(), - 'memberful_hide_admin_toolbar' => TRUE, - 'memberful_block_dashboard_access' => TRUE, - 'memberful_filter_account_menu_items' => TRUE, - 'memberful_auto_sync_display_names' => FALSE, - 'memberful_show_protected_content_in_search' => FALSE, - 'memberful_use_global_marketing' => FALSE, - 'memberful_use_global_snippets' => TRUE, - 'memberful_global_marketing_override' => TRUE, - 'memberful_global_marketing_content' => '', - 'memberful_ad_provider_settings' => array() + 'memberful_hide_admin_toolbar' => TRUE, + 'memberful_block_dashboard_access' => TRUE, + 'memberful_filter_account_menu_items' => TRUE, + 'memberful_auto_sync_display_names' => FALSE, + 'memberful_show_protected_content_in_search' => FALSE, + 'memberful_use_global_marketing' => FALSE, + 'memberful_use_global_snippets' => TRUE, + 'memberful_global_marketing_override' => TRUE, + 'memberful_global_marketing_content' => '', + 'memberful_ad_provider_settings' => array(), + Memberful_Paywall_Config::OPTION_KEY => array(), ); } diff --git a/wordpress/wp-content/plugins/memberful-wp/src/paywall.php b/wordpress/wp-content/plugins/memberful-wp/src/paywall.php new file mode 100644 index 00000000..03e81e75 --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/src/paywall.php @@ -0,0 +1,15 @@ + self::LUMINANCE_THRESHOLD ? self::TEXT_DARK : self::TEXT_LIGHT; + } + + /** + * Parse a 3- or 6-digit hex colour into [r, g, b] of 0–255 integers. + * + * @param string $hex Hex colour input. + * + * @return array{0:int,1:int,2:int}|null + */ + private static function hex_to_rgb( string $hex ): ?array { + $hex = ltrim( trim( $hex ), '#' ); + + if ( 3 === strlen( $hex ) ) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + + if ( 6 !== strlen( $hex ) || ! ctype_xdigit( $hex ) ) { + return null; + } + + return array( + (int) hexdec( substr( $hex, 0, 2 ) ), + (int) hexdec( substr( $hex, 2, 2 ) ), + (int) hexdec( substr( $hex, 4, 2 ) ), + ); + } +} \ No newline at end of file diff --git a/wordpress/wp-content/plugins/memberful-wp/src/paywall/config.php b/wordpress/wp-content/plugins/memberful-wp/src/paywall/config.php new file mode 100644 index 00000000..884290b8 --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/src/paywall/config.php @@ -0,0 +1,84 @@ + 'builder', + 'layout' => 'card', + 'heading' => __( 'Subscribe to keep reading', 'memberful' ), + 'subheading' => __( 'This post is for paying subscribers.', 'memberful' ), + 'features' => array(), + 'button_label' => __( 'Subscribe', 'memberful' ), + 'subscribe_url' => '', + 'sign_in_url' => '', + 'brand_color' => '', + 'background_color' => '', + 'button_shape' => 'square', + ); + } + + /** + * Read the stored config merged over defaults. + * + * On sites with legacy custom HTML in memberful_global_marketing_content and no stored builder config yet, the + * default mode swaps to custom_html so the existing content keeps rendering untouched. Once the user saves any + * config, the stored value wins and this check short-circuits. + * + * @return array + */ + public static function get(): array { + $stored = get_option( self::OPTION_KEY, array() ); + + if ( ! is_array( $stored ) ) { + $stored = array(); + } + + $defaults = self::defaults(); + if ( empty( $stored ) && self::has_legacy_content() ) { + $defaults['mode'] = 'custom_html'; + } + + return wp_parse_args( $stored, $defaults ); + } + + /** + * Whether the legacy marketing content option is populated. + * + * @return bool + */ + private static function has_legacy_content(): bool { + return '' !== trim( (string) get_option( 'memberful_global_marketing_content' ) ); + } + + /** + * Validate, sanitize, and persist a config payload. + * + * @param array $input Raw input (typically from the options form). + * + * @return bool True when the option was updated, false when unchanged or on failure. + */ + public static function save( array $input ): bool { + $clean = Memberful_Paywall_Sanitizer::sanitize( $input, self::defaults() ); + + return update_option( self::OPTION_KEY, $clean ); + } +} diff --git a/wordpress/wp-content/plugins/memberful-wp/src/paywall/preview.php b/wordpress/wp-content/plugins/memberful-wp/src/paywall/preview.php new file mode 100644 index 00000000..82c1f2b2 --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/src/paywall/preview.php @@ -0,0 +1,96 @@ + 'forbidden' ), 403 ); + } + + $raw = filter_input( INPUT_POST, 'config', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); + $raw = is_array( $raw ) ? $raw : array(); + + $config = Memberful_Paywall_Sanitizer::sanitize( $raw, Memberful_Paywall_Config::defaults() ); + + wp_send_json_success( array( 'html' => self::document( $config ) ) ); + } + + /** + * Wrap the rendered paywall HTML in a minimal document suitable for an iframe. + * + * @param array $config Sanitized config. + * + * @return string + */ + public static function document( array $config ): string { + $body = Memberful_Paywall_Renderer::render( $config ); + + $paywall_css = add_query_arg( 'ver', MEMBERFUL_VERSION, plugins_url( 'stylesheets/paywall.css', MEMBERFUL_PLUGIN_FILE ) ); + $theme_css = get_stylesheet_uri(); + + $links = sprintf( '', esc_url( $paywall_css ) ); + if ( ! empty( $theme_css ) ) { + $links .= sprintf( '', esc_url( $theme_css ) ); + } + + $layout = ( isset( $config['layout'] ) && in_array( $config['layout'], Memberful_Paywall_Config::LAYOUTS, true ) ) ? $config['layout'] : 'card'; + $teaser_class = 'memberful-global-teaser-content memberful-global-teaser-content--memberful-' . $layout; + $teaser = sprintf( + '', + esc_attr( $teaser_class ), + esc_html__( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae urna id quam faucibus gravida ac sed ipsum. Quisque eget velit dictum leo tempor bibendum nec sed odio.', 'memberful' ) + ); + + $styles = 'html,body{background:#fff;color:#1b1b1b;font-size:16px;line-height:1.6;margin:0;}' + . '.memberful-global-teaser-content{padding:24px 24px 0;}' + . '.memberful-global-teaser-content p{margin:0; padding-bottom: 1rem;}'; + + return '' + . '' + . '' + . '' + . '' + . $links + . '' + . '' + . '' . $teaser . $body . '' + . ''; + } + + /** + * AJAX args passed to the builder JS via wp_localize_script. + * + * @return array + */ + public static function script_args(): array { + return array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'action' => self::ACTION, + 'nonce' => wp_create_nonce( self::NONCE_KEY ), + ); + } +} + +Memberful_Paywall_Preview::register(); diff --git a/wordpress/wp-content/plugins/memberful-wp/src/paywall/renderer.php b/wordpress/wp-content/plugins/memberful-wp/src/paywall/renderer.php new file mode 100644 index 00000000..a7b12be7 --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/src/paywall/renderer.php @@ -0,0 +1,349 @@ +%3$s', + esc_attr( $layout ), + esc_attr( self::wrapper_style( $config ) ), + self::$method( $config ) + ); + } + + /** + * Render the "inline" layout - minimal text + CTA on a transparent band. + * + * @param array $config Sanitized config. + * + * @return string + */ + private static function render_inline( array $config ): string { + return '%s
', + esc_html( $config['subheading'] ) + ); + } + + /** + * Feature list with inline check icons, or empty when no features. + * + * @param array $config Sanitized config. + * + * @return string + */ + private static function features_block( array $config ): string { + if ( empty( $config['features'] ) ) { + return ''; + } + + $items = ''; + foreach ( $config['features'] as $feature ) { + $items .= '%s %s
', + esc_html__( 'Already a subscriber?', 'memberful' ), + esc_url( self::sign_in_url( $config ) ), + esc_html__( 'Sign in', 'memberful' ) + ); + } + + /** + * Circular lock badge shown at the top of the card layout. + * + * @return string + */ + private static function lock_badge(): string { + return ''; + } + + /** + * Wrapper inline style carrying the brand colour and button radius custom properties. + * + * @param array $config Sanitized config. + * + * @return string + */ + private static function wrapper_style( array $config ): string { + $parts = array( '--memberful-radius:' . self::button_radius( $config['button_shape'] ) ); + + $brand_color = isset( $config['brand_color'] ) ? sanitize_hex_color( (string) $config['brand_color'] ) : ''; + if ( ! empty( $brand_color ) ) { + $parts[] = '--memberful-brand:' . $brand_color; + } + + $background_color = isset( $config['background_color'] ) ? sanitize_hex_color( (string) $config['background_color'] ) : ''; + if ( ! empty( $background_color ) ) { + $parts[] = '--memberful-surface:' . $background_color; + $parts[] = '--memberful-text:' . Memberful_Paywall_Color::contrast_text_color( $background_color ); + } + + return implode( ';', $parts ) . ';'; + } + + /** + * Map the button-shape enum to a CSS radius. + * + * @param string $shape One of the `button_shape` enum values. + * + * @return string + */ + private static function button_radius( string $shape ): string { + switch ( $shape ) { + case 'pill': + return '999px'; + case 'square': + return '0'; + case 'rounded': + default: + return '8px'; + } + } + + /** + * Resolve the subscribe URL, falling back to the Memberful registration page. + * + * @param array $config Sanitized config. + * + * @return string + */ + private static function subscribe_url( array $config ): string { + return ! empty( $config['subscribe_url'] ) ? $config['subscribe_url'] : memberful_registration_page_url(); + } + + /** + * Resolve the sign-in URL, falling back to the Memberful sign-in endpoint. + * + * @param array $config Sanitized config. + * + * @return string + */ + private static function sign_in_url( array $config ): string { + return ! empty( $config['sign_in_url'] ) ? $config['sign_in_url'] : memberful_sign_in_url(); + } + + /** + * Inline check-mark SVG used in the features list. + * + * @return string + */ + private static function check_icon(): string { + return ''; + } + + /** + * Whether the builder paywall is the active rendering mode. + * + * @return bool + */ + private static function is_builder_mode(): bool { + $config = Memberful_Paywall_Config::get(); + + return 'builder' === $config['mode']; + } +} + +Memberful_Paywall_Renderer::register(); diff --git a/wordpress/wp-content/plugins/memberful-wp/src/paywall/sanitizer.php b/wordpress/wp-content/plugins/memberful-wp/src/paywall/sanitizer.php new file mode 100644 index 00000000..eab188dc --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/src/paywall/sanitizer.php @@ -0,0 +1,71 @@ + Memberful_Paywall_Config::MODES, + 'layout' => Memberful_Paywall_Config::LAYOUTS, + 'button_shape' => Memberful_Paywall_Config::BUTTON_SHAPES, + ); + + foreach ( $enums as $key => $allowed ) { + if ( isset( $input[ $key ] ) && in_array( $input[ $key ], $allowed, true ) ) { + $clean[ $key ] = $input[ $key ]; + } + } + + foreach ( array( 'heading', 'subheading', 'button_label' ) as $key ) { + if ( isset( $input[ $key ] ) ) { + $clean[ $key ] = sanitize_text_field( (string) $input[ $key ] ); + } + } + + if ( isset( $input['features'] ) ) { + $features = is_array( $input['features'] ) + ? $input['features'] + : preg_split( "/\r\n|\n|\r/", (string) $input['features'] ); + + $features = array_map( 'sanitize_text_field', (array) $features ); + $features = array_map( 'trim', $features ); + $features = array_values( array_filter( $features, 'strlen' ) ); + + $clean['features'] = $features; + } + + foreach ( array( 'subscribe_url', 'sign_in_url' ) as $key ) { + if ( isset( $input[ $key ] ) ) { + $clean[ $key ] = esc_url_raw( (string) $input[ $key ] ); + } + } + + foreach ( array( 'brand_color', 'background_color' ) as $color_key ) { + if ( isset( $input[ $color_key ] ) ) { + $color = sanitize_hex_color( (string) $input[ $color_key ] ); + if ( null !== $color && '' !== $color ) { + $clean[ $color_key ] = $color; + } + } + } + + return $clean; + } +} diff --git a/wordpress/wp-content/plugins/memberful-wp/stylesheets/admin.css b/wordpress/wp-content/plugins/memberful-wp/stylesheets/admin.css index 314e67b2..31e93b07 100755 --- a/wordpress/wp-content/plugins/memberful-wp/stylesheets/admin.css +++ b/wordpress/wp-content/plugins/memberful-wp/stylesheets/admin.css @@ -270,3 +270,380 @@ Ad Provider Settings .memberful-ad-provider-settings > div { margin-left: 1rem; } + +/*--------------------------------------------------------- +Paywall Builder +------------------------------------------------------------ */ +.memberful-bulk-apply-box--wide { + max-width: none; +} +.memberful-paywall-builder { + border-top: 1px solid #dcdcde; + margin-top: 2rem; + padding-top: 1.5rem; +} +.memberful-paywall-builder__section-heading { + display: block; + font-size: 13px; + font-weight: 600; + margin: 0 0 0.75rem; + padding: 0; + text-transform: none; +} + +/* Content source: tab-style segmented control */ +.memberful-paywall-builder__mode { + border: 0; + margin: 0 0 1.5rem; + padding: 0; +} +.memberful-paywall-builder__mode-tabs { + background: #fff; + border: 1px solid var(--wp-editor-canvas-background); + border-radius: 4px; + display: inline-flex; + overflow: hidden; +} +.memberful-paywall-builder__mode-tab { + cursor: pointer; + margin: 0; + position: relative; +} +.memberful-paywall-builder__mode-tab input[type="radio"] { + opacity: 0; + pointer-events: none; + position: absolute; +} +.memberful-paywall-builder__mode-tab span { + background: #fff; + border-right: 1px solid var(--wp-editor-canvas-background); + color: #2c3338; + display: inline-block; + font-size: 13px; + font-weight: 500; + line-height: 1.4; + padding: 8px 16px; +} +.memberful-paywall-builder__mode-tab:last-child span { + border-right: 0; +} +.memberful-paywall-builder__mode-tab input[type="radio"]:checked + span { + background: var(--wp-admin-theme-color); + color: #fff; +} +.memberful-paywall-builder__mode-tab input[type="radio"]:focus-visible + span { + box-shadow: inset 0 0 0 2px var(--wp-admin-theme-color); +} + +/* Template picker cards */ +.memberful-paywall-builder__layout { + border: 0; + margin: 0 0 1.75rem; + padding: 0; +} +.memberful-paywall-builder__template-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.memberful-paywall-builder__template-card { + cursor: pointer; + display: block; + margin: 0; + position: relative; +} +.memberful-paywall-builder__template-card input[type="radio"] { + opacity: 0; + pointer-events: none; + position: absolute; +} +.memberful-paywall-builder__template-card-inner { + background: #fff; + border: 2px solid #dcdcde; + border-radius: 6px; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + transition: border-color 120ms ease, box-shadow 120ms ease; +} +.memberful-paywall-builder__template-card:hover .memberful-paywall-builder__template-card-inner { + border-color: #8c8f94; +} +.memberful-paywall-builder__template-card input[type="radio"]:checked + .memberful-paywall-builder__template-card-inner { + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 1px var(--wp-admin-theme-color); +} +.memberful-paywall-builder__template-card input[type="radio"]:checked + .memberful-paywall-builder__template-card-inner::after { + background: var(--wp-admin-theme-color) url("data:image/svg+xml;utf8,") center/12px no-repeat; + border-radius: 50%; + content: ""; + height: 20px; + position: absolute; + right: 10px; + top: 10px; + width: 20px; +} +.memberful-paywall-builder__template-card input[type="radio"]:focus-visible + .memberful-paywall-builder__template-card-inner { + box-shadow: 0 0 0 2px var(--wp-admin-theme-color); +} + +.memberful-paywall-builder__template-thumb { + align-items: center; + background: #f0f0f1; + border-bottom: 1px solid #dcdcde; + display: flex; + flex-direction: column; + gap: 6px; + height: 96px; + justify-content: center; + position: relative; +} +.memberful-paywall-builder__template-thumb--inline { + background: #f6f7f7; +} +.memberful-paywall-builder__template-thumb--card { + background: #f0f0f1; +} +.memberful-paywall-builder__thumb-line { + background: var(--wp-editor-canvas-background); + border-radius: 2px; + height: 3px; + width: 100px; +} +.memberful-paywall-builder__thumb-button { + background: var(--wp-admin-theme-color); + border-radius: 2px; + height: 10px; + width: 48px; +} +.memberful-paywall-builder__thumb-lock { + background: var(--wp-admin-theme-color) url("data:image/svg+xml;utf8,") center/18px no-repeat; + border-radius: 50%; + height: 26px; + width: 26px; +} + +.memberful-paywall-builder__template-meta { + display: block; + padding: 10px 12px 12px; +} +.memberful-paywall-builder__template-meta strong { + color: #1d2327; + display: block; + font-size: 13px; + margin-bottom: 2px; +} +.memberful-paywall-builder__template-meta small { + color: #646970; + display: block; + font-size: 12px; + line-height: 1.35; +} + +/* Two-column layout: settings | preview */ +.memberful-paywall-builder__panel[data-panel="builder"] { + align-items: start; + display: grid; + gap: 32px; + grid-template-columns: minmax(0, 540px) minmax(0, 1fr); +} +.memberful-paywall-builder__preview { + align-self: start; + position: sticky; + top: 32px; +} +@media (max-width: 900px) { + .memberful-paywall-builder__panel[data-panel="builder"] { + grid-template-columns: minmax(0, 1fr); + } + .memberful-paywall-builder__preview { + position: static; + } +} + +.memberful-paywall-builder__customize .memberful-paywall-builder__field { + margin: 0 0 1rem; +} +.memberful-paywall-builder__customize label, +.memberful-paywall-builder__benefits-label, +.memberful-paywall-builder__button-shape-label { + color: #6b7280; + display: block; + font-size: 13px; + font-weight: 500; + margin-bottom: 4px; + text-transform: uppercase; +} +.memberful-paywall-builder__customize input[type="text"], +.memberful-paywall-builder__customize input[type="url"], +.memberful-paywall-builder__customize textarea { + box-sizing: border-box; + max-width: 100%; + width: 100%; +} +.memberful-paywall-builder__customize .description { + color: #646970; + display: block; + font-size: 12px; + margin-top: 4px; +} +.memberful-paywall-builder__field-row { + align-items: baseline; + display: flex; + justify-content: space-between; + margin-bottom: 4px; +} +.memberful-paywall-builder__field-row label { + margin: 0; +} +.memberful-paywall-builder__counter { + color: #646970; + font-size: 12px; +} +.memberful-paywall-builder__customize .memberful-paywall-builder__field--paired { + align-items: start; + display: grid; + gap: 12px; + grid-template-columns: 1fr auto; + margin: 0 0 1rem; +} +.memberful-paywall-builder__field--paired .memberful-paywall-builder__field-main, +.memberful-paywall-builder__field--paired .memberful-paywall-builder__field-aside { + margin: 0; +} +.memberful-paywall-builder__field--paired .memberful-paywall-builder__field-aside select { + min-width: 80px; +} + +/* Accent + Background colours */ +.memberful-paywall-builder__field .wp-color-result.button { + margin: 0; +} + +/* Button shape controls */ +.memberful-paywall-builder__segmented { + border: 1px solid var(--wp-editor-canvas-background); + border-radius: 6px; + display: inline-flex; + padding: 4px; +} +.memberful-paywall-builder__segmented-option { + cursor: pointer; + margin: 0 !important; +} +.memberful-paywall-builder__segmented-option input[type="radio"] { + opacity: 0; + pointer-events: none; + position: absolute; +} +.memberful-paywall-builder__segmented-option-inner { + align-items: center; + border-radius: 4px; + color: #646970; + display: inline-flex; + font-size: 12px; + gap: 6px; + padding: 5px 14px 5px 10px; + transition: background-color 120ms ease, color 120ms ease; +} +.memberful-paywall-builder__segmented-option:hover .memberful-paywall-builder__segmented-option-inner { + color: #1d2327; +} +.memberful-paywall-builder__segmented-option input[type="radio"]:checked + .memberful-paywall-builder__segmented-option-inner { + background: var(--wp-admin-theme-color); + color: #fff; +} +.memberful-paywall-builder__segmented-option input[type="radio"]:focus-visible + .memberful-paywall-builder__segmented-option-inner { + box-shadow: 0 0 0 2px var(--wp-admin-theme-color); +} +.memberful-paywall-builder__segmented-option-inner svg { + flex: 0 0 auto; +} + +/* Benefits */ +.memberful-paywall-builder__benefits-label, +.memberful-paywall-builder__button-shape-label { + margin-bottom: 6px; + padding: 0; +} +.memberful-paywall-builder__benefit-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 8px; +} +.memberful-paywall-builder__benefit { + align-items: center; + display: flex; + gap: 8px; +} +.memberful-paywall-builder__benefit-icon { + color: var(--wp-admin-theme-color); + flex: 0 0 auto; +} +.memberful-paywall-builder__customize .memberful-paywall-builder__benefit-label { + flex: 1; + margin: 0; +} +.memberful-paywall-builder__benefit-remove { + background: none; + border: 0; + color: #646970; + cursor: pointer; + display: inline-flex; + opacity: 0; + padding: 4px; + transition: opacity 120ms ease; +} +.memberful-paywall-builder__benefit:hover .memberful-paywall-builder__benefit-remove, +.memberful-paywall-builder__benefit-remove:focus-visible { + opacity: 1; +} +.memberful-paywall-builder__benefit-remove:hover { + color: #1d2327; +} +.memberful-paywall-builder__benefit-add { + background: none; + border: 0; + color: var(--wp-admin-theme-color); + cursor: pointer; + font-weight: 500; + padding: 4px 0; +} +.memberful-paywall-builder__benefit-add:hover { + color: var(--wp-admin-theme-color-darker-10); +} + +/* Advanced settings */ +.memberful-paywall-builder__advanced { + border-top: 1px solid var(--wp-editor-canvas-background); + margin-top: 1.5rem; + padding-top: 1rem; +} +.memberful-paywall-builder__advanced-summary { + align-items: center; + cursor: pointer; + display: flex; + font-weight: 600; + justify-content: space-between; + margin-bottom: 1rem; +} +.memberful-paywall-builder__advanced-summary::-webkit-details-marker { + display: none; +} +.memberful-paywall-builder__advanced-chevron { + color: #646970; + transition: transform 150ms ease; +} +.memberful-paywall-builder__advanced[open] .memberful-paywall-builder__advanced-chevron { + transform: rotate(180deg); +} + +.memberful-paywall-builder__preview-frame { + background: #fff; + border: 1px solid #dcdcde; + border-radius: 4px; + min-height: 500px; + width: 100%; +} diff --git a/wordpress/wp-content/plugins/memberful-wp/stylesheets/paywall.css b/wordpress/wp-content/plugins/memberful-wp/stylesheets/paywall.css new file mode 100644 index 00000000..1981f55f --- /dev/null +++ b/wordpress/wp-content/plugins/memberful-wp/stylesheets/paywall.css @@ -0,0 +1,178 @@ +/** + * Memberful paywall — frontend + admin preview styles. + */ + +.memberful-paywall { + --memberful-border: #e5e5e5; + --memberful-brand: var(--wp--preset--color--primary, var(--wp--preset--color--accent, var(--wp-admin-theme-color, #2271b1))); + --memberful-radius: 0; + --memberful-surface: #fff; + --memberful-text: #1a1a1a; + --memberful-muted: color-mix(in srgb, var(--memberful-text) 60%, transparent); + + box-sizing: border-box; + color: var(--memberful-text); + font-family: inherit; + line-height: 1.5; + margin: 0; + padding: 0; + position: relative; + width: 100%; +} + +.memberful-paywall *, +.memberful-paywall *::before, +.memberful-paywall *::after { + box-sizing: border-box; +} + +.memberful-paywall__inner { + margin: 0 auto; + max-width: 600px; + padding: var(--wp--preset--spacing--50, 2rem) var(--wp--preset--spacing--40, 1.5rem) var(--wp--preset--spacing--40, 1.5rem); + text-align: center; +} + +.memberful-paywall__heading { + color: var(--memberful-text); + font-weight: 600; + line-height: 1.3; + margin: 0 0 0.5rem; +} + +.memberful-paywall__heading { + font-size: var(--wp--preset--font-size--large, 1.375rem); +} + +.memberful-paywall__subheading { + color: var(--memberful-muted); + font-size: var(--wp--preset--font-size--small, 0.875rem); + margin: 0 auto 1.25rem; + max-width: 28rem; +} + +.memberful-paywall__features { + color: var(--memberful-muted); + display: inline-flex; + flex-direction: column; + font-size: 0.9375rem; + gap: 0.375rem; + list-style: none; + margin: 0 0 1.25rem; + padding: 0; + text-align: left; +} + +.memberful-paywall__features li { + align-items: flex-start; + display: flex; + gap: 0.5rem; +} + +.memberful-paywall__check { + color: var(--memberful-brand); + flex: 0 0 auto; + margin-top: 0.2rem; +} + +.memberful-paywall__actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; + margin-bottom: 0; +} + +.memberful-paywall__button { + border: 1px solid transparent; + border-radius: var(--memberful-radius); + display: inline-block; + font-size: var(--wp--preset--font-size--small, 0.875rem); + font-weight: 600; + line-height: 1.2; + padding: 0.5rem 1.5rem; + text-decoration: none; + transition: background-color 150ms ease, color 150ms ease, filter 150ms ease; +} + +.memberful-paywall__button--primary { + background: var(--memberful-brand); + border-color: var(--memberful-brand); + color: #fff; +} + +.memberful-paywall__button--primary:hover, +.memberful-paywall__button--primary:focus { + filter: brightness(0.92); +} + +.memberful-paywall__signin { + color: var(--memberful-muted); + font-size: 0.75rem; + margin: var(--wp--preset--spacing--30, 1rem) 0 0 !important; /* fixes compatibility with some themes */ +} + +.memberful-paywall__signin-link { + color: var(--memberful-brand); + text-decoration: underline; +} + +.memberful-paywall__lock { + align-items: center; + background: var(--memberful-brand); + border-radius: 50%; + color: #fff; + display: inline-flex; + height: 40px; + justify-content: center; + margin: 0 auto 1rem; + width: 40px; +} + +/* Inline - top-divider band on white. */ +.memberful-paywall--inline { + background: var(--memberful-surface); + border-top: 2px solid var(--memberful-brand); +} + +/* Card - centred card surface on a fixed muted page. */ +.memberful-paywall--card .memberful-paywall__inner { + padding: var(--wp--preset--spacing--40, 1.5rem); +} + +.memberful-paywall--card .memberful-paywall__card { + background: var(--memberful-surface); + border: 1px solid #e0e0e0; + border-radius: 1rem; + box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px; + margin: 0 auto; + max-width: 34rem; + padding: var(--wp--preset--spacing--50, 2rem); +} + +.memberful-paywall--card .memberful-paywall__heading { + font-weight: 700; +} + +.memberful-paywall--card .memberful-paywall__button--primary { + display: block; + padding: 0.625rem 1.5rem; + width: 100%; +} + +/* Teaser fade. */ +.memberful-global-teaser-content[class*="--memberful-"] { + position: relative; +} + +.memberful-global-teaser-content[class*="--memberful-"]::after { + background: linear-gradient(transparent, var(--wp--preset--color--background, #fff)); + bottom: 0; + content: ""; + height: 4rem; + left: 0; + pointer-events: none; + position: absolute; + right: 0; +} diff --git a/wordpress/wp-content/plugins/memberful-wp/views/global_marketing.php b/wordpress/wp-content/plugins/memberful-wp/views/global_marketing.php index 7b8225d3..03b3d4e2 100644 --- a/wordpress/wp-content/plugins/memberful-wp/views/global_marketing.php +++ b/wordpress/wp-content/plugins/memberful-wp/views/global_marketing.php @@ -1,72 +1,67 @@ -+ global marketing settings.', 'memberful' ), + array( 'a' => array( 'href' => array() ) ) + ), + esc_url( memberful_wp_plugin_global_marketing_url() ) + ); + ?> +
++