diff --git a/includes/class-blocks.php b/includes/class-blocks.php index 4a2adf28a9..4a3be8e3f8 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -10,6 +10,8 @@ defined( 'ABSPATH' ) || exit; use Newspack\Optional_Modules\Collections; +use Newspack\Memberships; +use Newspack\Memberships\Metering; /** * Newspack Blocks Class. @@ -20,6 +22,7 @@ final class Blocks { */ public static function init() { require_once NEWSPACK_ABSPATH . 'src/blocks/reader-registration/index.php'; + require_once NEWSPACK_ABSPATH . 'src/blocks/content-gate-countdown/class-content-gate-countdown-block.php'; if ( wp_is_block_theme() && class_exists( 'Newspack\Corrections' ) ) { require_once NEWSPACK_ABSPATH . 'src/blocks/correction-box/class-correction-box-block.php'; @@ -49,22 +52,32 @@ public static function enqueue_block_editor_assets() { NEWSPACK_PLUGIN_VERSION, true ); + $script_data = [ + 'has_newsletters' => class_exists( 'Newspack_Newsletters_Subscription' ), + 'has_reader_activation' => Reader_Activation::is_enabled(), + 'newsletters_url' => Wizards::get_wizard( 'newsletters' )->newsletters_settings_url(), + 'has_google_oauth' => Google_OAuth::is_oauth_configured(), + 'google_logo_svg' => \Newspack\Newspack_UI_Icons::get_svg( 'google' ), + 'reader_activation_terms' => Reader_Activation::get_setting( 'terms_text' ), + 'reader_activation_url' => Reader_Activation::get_setting( 'terms_url' ), + 'has_recaptcha' => Recaptcha::can_use_captcha(), + 'recaptcha_url' => admin_url( 'admin.php?page=newspack-settings' ), + 'corrections_enabled' => wp_is_block_theme() && class_exists( 'Newspack\Corrections' ), + 'collections_enabled' => Collections::is_module_active(), + 'has_memberships' => Memberships::is_active(), + ]; + if ( $script_data['has_memberships'] ) { + $script_data['content_gate_data'] = [ + 'anonymous_metered_views' => Metering::get_total_metered_views( false ), + 'loggedin_metered_views' => Metering::get_total_metered_views( true ), + 'metered_views' => Metering::get_metered_views(), + 'metering_period' => Metering::get_metering_period(), + ]; + } \wp_localize_script( 'newspack-blocks', 'newspack_blocks', - [ - 'has_newsletters' => class_exists( 'Newspack_Newsletters_Subscription' ), - 'has_reader_activation' => Reader_Activation::is_enabled(), - 'newsletters_url' => Wizards::get_wizard( 'newsletters' )->newsletters_settings_url(), - 'has_google_oauth' => Google_OAuth::is_oauth_configured(), - 'google_logo_svg' => \Newspack\Newspack_UI_Icons::get_svg( 'google' ), - 'reader_activation_terms' => Reader_Activation::get_setting( 'terms_text' ), - 'reader_activation_url' => Reader_Activation::get_setting( 'terms_url' ), - 'has_recaptcha' => Recaptcha::can_use_captcha(), - 'recaptcha_url' => admin_url( 'admin.php?page=newspack-settings' ), - 'corrections_enabled' => wp_is_block_theme() && class_exists( 'Newspack\Corrections' ), - 'collections_enabled' => Collections::is_module_active(), - ] + $script_data ); \wp_enqueue_style( 'newspack-blocks', diff --git a/includes/plugins/wc-memberships/class-memberships.php b/includes/plugins/wc-memberships/class-memberships.php index 6434f4a3d8..a0e1315eea 100644 --- a/includes/plugins/wc-memberships/class-memberships.php +++ b/includes/plugins/wc-memberships/class-memberships.php @@ -686,6 +686,29 @@ public static function get_restricted_post_excerpt( $post ) { $content = explode( '', $content )[0]; } else { $count = (int) get_post_meta( $gate_post_id, 'visible_paragraphs', true ); + // Remove all spaces. + $content = preg_replace( '/\s+/', ' ', $content ); + /** + * Filter the list of blocks to exclude from the excerpt. + * + * @param array $excluded_blocks Array of blocks to exclude. i.e. [ 'core/image', 'newspack/content-gate-countdown' ]. + * + * @return array + */ + $excluded_blocks = apply_filters( 'newspack_memberships_excerpt_excluded_blocks', [ 'newspack/content-gate-countdown' ] ); + // Remove unwanted blocks from the content. + foreach ( $excluded_blocks as $block ) { + [ $category, $name ] = explode( '/', $block ); + if ( ! $category || ! $name ) { + continue; + } + if ( 'core' === $category ) { + $regex = $name; + } else { + $regex = "$category\/$name"; + } + $content = preg_replace( "/.*?/s", '', $content ); + } // Split into paragraphs. $content = explode( '

', $content ); // Extract the first $x paragraphs only. diff --git a/includes/plugins/wc-memberships/class-metering.php b/includes/plugins/wc-memberships/class-metering.php index f5049e1451..25826e9310 100644 --- a/includes/plugins/wc-memberships/class-metering.php +++ b/includes/plugins/wc-memberships/class-metering.php @@ -298,5 +298,65 @@ public static function get_article_view( $activity ) { self::$article_view = $activity; return $activity; } + + /** + * Get the metering period for a post. + * + * @param int|null $post_id Post ID. Default is current post. + * + * @return string Metered period (day, week, month). + */ + public static function get_metering_period( $post_id = null ) { + if ( ! $post_id ) { + $post_id = get_the_ID(); + } + $gate_post_id = Memberships::get_gate_post_id( $post_id ); + return \get_post_meta( $gate_post_id, 'metering_period', true ); + } + + /** + * Get number of metered views for the user. + * + * @param int|null $user_id User ID. Default is current user. + * + * @return int Number of metered views. + */ + public static function get_metered_views( $user_id = null ) { + if ( ! $user_id ) { + $user_id = get_current_user_id(); + } + // For anonymous users, return the anonymous count. + if ( ! $user_id ) { + $gate_post_id = Memberships::get_gate_post_id(); + return (int) \get_post_meta( $gate_post_id, 'metering_anonymous_count', true ); + } + + // For logged-in users, calculate the remaining views based on their metering data. + $gate_post_id = Memberships::get_gate_post_id(); + $user_meta_key = self::METERING_META_KEY . '_' . $gate_post_id; + $user_metering_data = \get_user_meta( $user_id, $user_meta_key, true ); + if ( ! is_array( $user_metering_data ) || ! isset( $user_metering_data['content'] ) ) { + return $count; + } + return count( $user_metering_data['content'] ); + } + + /** + * Get total number of metered views for current post. + * + * @param boolean $is_logged_in Whether to check for logged-in or anonymous users. Default is false (anonymous). + * + * @return int|boolean Total number of metered views if metering is enabled, otherwise false. + */ + public static function get_total_metered_views( $is_logged_in = false ) { + $gate_post_id = Memberships::get_gate_post_id( get_the_ID() ); + if ( ! $gate_post_id ) { + return false; + } + if ( ! $is_logged_in ) { + return (int) \get_post_meta( $gate_post_id, 'metering_anonymous_count', true ); + } + return (int) \get_post_meta( $gate_post_id, 'metering_registered_count', true ); + } } Metering::init(); diff --git a/packages/icons/index.js b/packages/icons/index.js index b7819aec75..296fdf4311 100644 --- a/packages/icons/index.js +++ b/packages/icons/index.js @@ -33,3 +33,4 @@ export { default as searchEmpty } from './src/search-empty'; export { default as tabItem } from './src/tab-item'; export { default as tabs } from './src/tabs'; export { default as theme } from './src/theme'; +export { default as countdown } from './src/content-gate-countdown'; diff --git a/packages/icons/src/content-gate-countdown.js b/packages/icons/src/content-gate-countdown.js new file mode 100644 index 0000000000..07bd8cc652 --- /dev/null +++ b/packages/icons/src/content-gate-countdown.js @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const countdown = ( + + + + +); + +export default countdown; diff --git a/src/blocks/content-gate-countdown/block.json b/src/blocks/content-gate-countdown/block.json new file mode 100644 index 0000000000..36f62e83ea --- /dev/null +++ b/src/blocks/content-gate-countdown/block.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "newspack/content-gate-countdown", + "category": "newspack", + "attributes": { + "text": { + "type": "string", + "default": "" + } + }, + "supports": { + "align": [ "wide", "full" ], + "html": false, + "color": { + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "textAlign": true, + "__experimentalFontFamily": true, + "__experimentalTextDecoration": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalWritingMode": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + }, + "textdomain": "newspack-plugin" +} diff --git a/src/blocks/content-gate-countdown/class-content-gate-countdown-block.php b/src/blocks/content-gate-countdown/class-content-gate-countdown-block.php new file mode 100644 index 0000000000..4f3ace444b --- /dev/null +++ b/src/blocks/content-gate-countdown/class-content-gate-countdown-block.php @@ -0,0 +1,110 @@ + [ __CLASS__, 'render_block' ], + ] + ); + } + + /** + * Block render callback. + * + * @param array $attributes The block attributes. + * @param string $content The block content. + * + * @return string The block HTML. + */ + public static function render_block( array $attributes, string $content ) { + if ( ! Metering::is_metering() || ! Memberships::is_post_restricted() ) { + return ''; + } + $total_views = Metering::get_total_metered_views( \is_user_logged_in() ); + if ( false === $total_views ) { + return ''; + } + $views = Metering::get_metered_views( get_current_user_id() ); + $countdown = sprintf( + /* translators: 1: current number of metered views, 2: total metered views. */ + __( '%1$d/%2$d', 'newspack-plugin' ), + $views, + $total_views + ); + $text = isset( $attributes['text'] ) ? esc_html( $attributes['text'] ) : ''; + if ( empty( $text ) ) { + $text = sprintf( + /* translators: %s - metered content period (week, month, etc. */ + __( + 'free articles this %s', + 'newspack-plugin' + ), + Metering::get_metering_period() + ); + } + $block_wrapper_attributes = get_block_wrapper_attributes( + [ + 'class' => 'newspack-content-gate-countdown__wrapper', + ] + ); + $block_content = "
+
+
+ $countdown +

$text

+
+ $content +
+
"; + + return $block_content; + } +} + +Content_Gate_Countdown_Block::init(); diff --git a/src/blocks/content-gate-countdown/edit.jsx b/src/blocks/content-gate-countdown/edit.jsx new file mode 100644 index 0000000000..8ab843ea1e --- /dev/null +++ b/src/blocks/content-gate-countdown/edit.jsx @@ -0,0 +1,112 @@ +/* globals newspack_blocks */ + +/** + * WordPress dependencies + */ +import { useBlockProps, useInnerBlocksProps, InspectorControls } from '@wordpress/block-editor'; +import { PanelBody, Placeholder, TextareaControl } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { caution } from '@wordpress/icons'; +import { useState } from '@wordpress/element'; + +/** + * Edit function for the Content Gate Countdown block. + * + * @return {JSX.Element} The Content Gate Countdown block. + */ +export default function Edit( { attributes, setAttributes } ) { + const blockProps = useBlockProps( { className: 'newspack-content-gate-countdown__wrapper' } ); + const { children, ...innerBlockProps } = useInnerBlocksProps( + { className: 'newspack-content-gate-countdown__actions' }, + { + allowedBlocks: [ 'core/paragraph', 'core/heading', 'core/buttons', 'newspack-blocks/checkout-button' ], + template: [ + [ + 'core/paragraph', + { + align: 'center', + content: __( 'Get unlimited access.', 'newspack-plugin' ), + style: { typography: { fontWeight: '700' } }, + }, + ], + [ + 'newspack-blocks/checkout-button', + { + text: __( 'Subscribe now', 'newspack-plugin' ), + align: 'center', + backgroundColor: 'primary', + textColor: 'secondary', + }, + ], + ], + } + ); + const { + metering_period: meteringPeriod, + loggedin_metered_views: loggedinViews, + anonymous_metered_views: anonymousViews, + metered_views: views, + } = newspack_blocks.content_gate_data || {}; + const [ text, setText ] = useState( + attributes.text + ? attributes.text + : sprintf( + /* translators: %s is the metered period, e.g. "month" or "week". */ + __( 'free articles this %s', 'newspack-plugin' ), + meteringPeriod + ) + ); + // Admin is always logged in, so if no loggedin metered views are set, use the anonymous views instead. + const totalViews = loggedinViews > 0 ? loggedinViews : anonymousViews; + const handleChange = value => { + setAttributes( { text: value } ); + setText( value ); + }; + + if ( ! totalViews ) { + return ( +
+ +
+ ); + } + + return ( + <> + + + + + + +
+
+
+ + { sprintf( + /* translators: 1: current number of metered views, 2: total metered views. */ __( '%1$d/%2$d', 'newspack-plugin' ), + parseInt( views ), + parseInt( totalViews ) + ) } + +

{ text }

+
+
{ children }
+
+
+ + ); +} diff --git a/src/blocks/content-gate-countdown/index.js b/src/blocks/content-gate-countdown/index.js new file mode 100644 index 0000000000..e4895660dc --- /dev/null +++ b/src/blocks/content-gate-countdown/index.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { useInnerBlocksProps } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; +import { countdown as icon } from '../../../packages/icons'; +import colors from '../../../packages/colors/colors.module.scss'; +import './style.scss'; + +export const title = __( 'Content Gate Countdown', 'newspack-plugin' ); + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + title, + icon: { + src: icon, + foreground: colors[ 'primary-400' ], + }, + keywords: [ __( 'countdown', 'newspack-plugin' ), __( 'content gate', 'newspack-plugin' ) ], + description: __( 'A countdown for content gate metering functionality.', 'newspack-plugin' ), + edit: Edit, + save: () => ( +
+ ), +}; diff --git a/src/blocks/content-gate-countdown/style.scss b/src/blocks/content-gate-countdown/style.scss new file mode 100644 index 0000000000..6ab4eab754 --- /dev/null +++ b/src/blocks/content-gate-countdown/style.scss @@ -0,0 +1,71 @@ +$breakpoint-md: 782px; + +.newspack-content-gate-countdown__wrapper { + width: 100%; + + .newspack-content-gate-countdown { + &__content { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1em; + padding: 1rem; + font-family: var(--newspack-ui-font-family), system-ui, sans-serif !important; + box-shadow: + 0 5px 10px var(--newspack-ui-color-neutral-10, rgba(0, 0, 0, 0.1)), + 0 2px 5px var(--newspack-ui-color-neutral-20, rgba(0, 0, 0, 0.06)); + + @media screen and ( min-width: $breakpoint-md ) { + flex-direction: row; + justify-content: flex-start; + } + } + + &__text { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + + p { + font-size: 0.8em; + text-transform: uppercase; + opacity: 0.5; + } + + @media screen and ( min-width: $breakpoint-md ) { + width: 200px; + justify-content: flex-start; + } + } + + &__countdown { + display: flex; + font-size: 1.5em; + font-weight: 700; + max-width: 100px; + } + + &__actions { + width: 100%; + padding-right: 0; + + & > *:first-child { + margin-top: 0; + } + + & > *:last-child { + margin-bottom: 0; + } + + @media screen and ( min-width: $breakpoint-md ) { + width: calc(100% - 200px); + padding-right: calc(1em + 200px); + } + } + } +} diff --git a/src/blocks/content-gate-countdown/view.js b/src/blocks/content-gate-countdown/view.js new file mode 100644 index 0000000000..ff3a5876eb --- /dev/null +++ b/src/blocks/content-gate-countdown/view.js @@ -0,0 +1,39 @@ +/* globals newspack_metering_settings */ + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { domReady } from '../../utils'; + +domReady( () => { + if ( typeof newspack_metering_settings === 'undefined' ) { + return; + } + const { count, gate_id } = newspack_metering_settings; + window.newspackRAS = window.newspackRAS || []; + window.newspackRAS.push( ras => { + const { authenticated } = ras?.getReader() || { authenticated: false }; + if ( authenticated ) { + return; + } + const storeKey = 'metering-' + ( gate_id || 0 ); + const { content } = ras?.store?.get( storeKey ) || { content: [] }; + const countdownEl = document.querySelector( '.newspack-content-gate-countdown__countdown' ); + if ( ! countdownEl ) { + return; + } + // Replace countdown for anonymous users. + const countdown = sprintf( + /* translators: 1: current number of metered views, 2: total metered views. */ __( '%1$d/%2$d', 'newspack-plugin' ), + content.length, + count + ); + countdownEl.textContent = countdown; + } ); +} ); diff --git a/src/blocks/index.js b/src/blocks/index.js index 020b425f0e..7db217d08b 100644 --- a/src/blocks/index.js +++ b/src/blocks/index.js @@ -13,17 +13,19 @@ import * as correctionBox from './correction-box'; import * as correctionItem from './correction-item'; import * as avatar from './avatar'; import * as collections from './collections'; +import * as contentGateCountdown from './content-gate-countdown'; /** * Block Scripts */ import './core-image'; -export const blocks = [ readerRegistration, correctionBox, correctionItem, avatar, collections ]; +export const blocks = [ readerRegistration, correctionBox, correctionItem, avatar, collections, contentGateCountdown ]; const readerActivationBlocks = [ 'newspack/reader-registration' ]; const correctionBlocks = [ 'newspack/correction-box', 'newspack/correction-item' ]; const collectionsBlocks = [ 'newspack/collections' ]; +const contentGateBlocks = [ 'newspack/content-gate-countdown' ]; /** * Function to register an individual block. @@ -41,16 +43,18 @@ const registerBlock = block => { if ( readerActivationBlocks.includes( name ) && ! newspack_blocks.has_reader_activation ) { return; } - /** Do not register correction blocks if it's disabled. */ if ( correctionBlocks.includes( name ) && ! newspack_blocks.corrections_enabled ) { return; } - /** Do not register collections blocks if Collections module is disabled. */ if ( collectionsBlocks.includes( name ) && ! newspack_blocks.collections_enabled ) { return; } + /** Do not register content gate blocks if Memberships is not active. */ + if ( contentGateBlocks.includes( name ) && ! newspack_blocks.has_memberships ) { + return; + } registerBlockType( { name, ...metadata }, settings ); }; diff --git a/webpack.config.js b/webpack.config.js index e02c3b15ca..ea32219588 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -45,6 +45,7 @@ const entry = { 'reader-registration-block': path.join( __dirname, 'src', 'blocks', 'reader-registration', 'view.js' ), 'correction-box-block': path.join( __dirname, 'src', 'blocks', 'correction-box', 'index.js' ), 'correction-item-block': path.join( __dirname, 'src', 'blocks', 'correction-item', 'index.js' ), + 'content-gate-countdown-block': path.join( __dirname, 'src', 'blocks', 'content-gate-countdown', 'view.js' ), 'avatar-block': path.join( __dirname, 'src', 'blocks', 'avatar', 'index.js' ), 'my-account': path.join( __dirname, 'src', 'my-account', 'index.js' ), 'my-account-v0': path.join( __dirname, 'src', 'my-account', 'v0', 'index.js' ),