Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions includes/content-gate/class-content-gate.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public static function init() {
include __DIR__ . '/class-content-restriction-control.php';
include __DIR__ . '/class-block-patterns.php';
include __DIR__ . '/class-metering.php';
include __DIR__ . '/class-metering-countdown.php';
include __DIR__ . '/content-gifting/class-content-gifting.php';
}

Expand Down
284 changes: 284 additions & 0 deletions includes/content-gate/class-metering-countdown.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
<?php
/**
* WooCommerce Content Gate metering countdown banner.
*
* @package Newspack
*/

namespace Newspack;

/**
* WooCommerce Content Gate metering countdown banner class.
*/
class Metering_Countdown {

const OPTION_PREFIX = 'np_countdown_banner_';

/**
* Initialize hooks.
*/
public static function init() {
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
add_action( 'wp_footer', [ __CLASS__, 'print_cta' ] );
add_filter( 'body_class', [ __CLASS__, 'filter_body_class' ] );
add_filter( 'newspack_ads_placement_data', [ __CLASS__, 'filter_ads_placement_data' ], 10, 2 );
}

/**
* Get all settings with default values for the countdown banner.
*
* @return array Default countdown settings.
*/
public static function get_default_settings() {
return [
'enabled' => false,
'style' => 'light',
'cta_label' => __( 'Subscribe now and get unlimited access.', 'newspack-plugin' ),
'button_label' => __( 'Subscribe now', 'newspack-plugin' ),
'cta_url' => '',
];
}

/**
* Get all settings for the countdown banner.
*
* @param string $key Optional key to get a specific setting. If not provided, all settings will be returned.
*
* @return array|mixed Countdown banner settings, or a specific setting if a key is provided.
*/
public static function get_settings( $key = null ) {
$settings = self::get_default_settings();
if ( $key && isset( $settings[ $key ] ) ) {
return get_option( self::OPTION_PREFIX . $key, $settings[ $key ] );
}
foreach ( $settings as $key => $value ) {
$settings[ $key ] = get_option( self::OPTION_PREFIX . $key, $value );
}
return $settings;
}

/**
* Sanitize a setting.
*
* @param string $key The setting key.
* @param mixed $value The setting value.
*
* @return mixed The sanitized setting value or WP_Error if setting key is invalid.
*/
public static function sanitize_setting( $key, $value ) {
$default_settings = self::get_default_settings();
if ( ! isset( $default_settings[ $key ] ) ) {
// translators: %s is the setting key.
return new \WP_Error( 'newspack_countdown_banner_invalid_setting', sprintf( __( 'Invalid setting key: %s.', 'newspack-plugin' ), $key ) );
}
if ( $key === 'style' && ! in_array( $value, [ 'light', 'dark' ], true ) ) {
return $default_settings[ $key ];
}
if ( $key === 'cta_url' ) {
return sanitize_url( $value );
}
if ( is_bool( $default_settings[ $key ] ) ) {
return boolval( $value );
}
return sanitize_text_field( $value );
}

/**
* Update settings for the countdown banner.
*
* @param array $settings New countdown settings.
*
* @return array|\WP_Error Updated countdown settings or error if update fails.
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The @return documentation states it returns array|\WP_Error but the function never returns a \WP_Error. It always returns the $current_settings array. Either the documentation should be updated to only indicate @return array, or error handling should be implemented to actually return a \WP_Error when appropriate.

Suggested change
* @return array|\WP_Error Updated countdown settings or error if update fails.
* @return array Updated countdown settings.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@dkoo dkoo Dec 4, 2025

Choose a reason for hiding this comment

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

Fixed in dbb794b to actually return an error if there is one

*/
public static function update_settings( $settings ) {
$current_settings = self::get_settings();
foreach ( $settings as $key => $value ) {
$sanitized = self::sanitize_setting( $key, $value );
if ( is_wp_error( $sanitized ) ) {
return $sanitized;
}
if ( $sanitized === $current_settings[ $key ] ) {
continue;
}
$updated = update_option( self::OPTION_PREFIX . $key, $sanitized );
if ( ! $updated ) {
return new \WP_Error( 'newspack_countdown_banner_update_failed', __( 'Failed to update countdown banner settings.', 'newspack-plugin' ) );
}
$current_settings[ $key ] = $sanitized;
}
return $current_settings;
}

/**
* Whether the countdown banner should be displayed.
*
* @return bool
*/
public static function is_enabled() {
return self::get_settings( 'enabled' ) && Metering::is_metering() && is_singular();
}

/**
* Enqueue assets.
*/
public static function enqueue_assets() {
// Enqueue assets only if enabled and the post is metered.
if ( ! self::is_enabled() ) {
return;
}
$asset = require_once dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/content-banner.asset.php';

// Ensure the content gate metering script is enqueued first.
$asset['dependencies'][] = 'newspack-content-gate-metering';
wp_enqueue_script( 'newspack-content-banner', Newspack::plugin_url() . '/dist/content-banner.js', $asset['dependencies'], NEWSPACK_PLUGIN_VERSION, true );
wp_enqueue_style( 'newspack-content-banner', Newspack::plugin_url() . '/dist/content-banner.css', [], NEWSPACK_PLUGIN_VERSION );
}

/**
* Print the subscribe button.
*/
public static function print_subscribe_button() {
if ( ! class_exists( 'Newspack_Blocks' ) || ! class_exists( 'Newspack_Blocks\Modal_Checkout' ) || ! class_exists( 'Newspack_Blocks\Modal_Checkout\Checkout_Data' ) || ! function_exists( 'wc_get_product' ) ) {
return;
}
$settings = self::get_settings();
$button_label = $settings['button_label'];
$button_class = 'dark' === $settings['style'] ? 'newspack-ui__button--primary-light' : 'newspack-ui__button--accent';

$cta_url = $settings['cta_url'];
if ( $cta_url ) {
?>
<a href="<?php echo esc_url( $cta_url ); ?>" class="newspack-ui__button newspack-ui__button--x-small <?php echo esc_attr( $button_class ); ?>"><?php echo esc_html( $button_label ); ?></a>
<?php
return;
}

// If CTA url is not provided, try a modal checkout using the primary subscription tier product.
$product = Subscriptions_Tiers::get_primary_subscription_tier_product();
if ( ! $product ) {
return;
}
\Newspack_Blocks\Modal_Checkout::enqueue_modal( $product->get_id() );
\Newspack_Blocks::enqueue_view_assets( 'checkout-button' );
$checkout_data = \Newspack_Blocks\Modal_Checkout\Checkout_Data::get_checkout_data( $product );
?>
<div class="wp-block-newspack-blocks-checkout-button">
<form data-checkout="<?php echo esc_attr( wp_json_encode( $checkout_data ) ); ?>" target="newspack_modal_checkout_iframe">
<input type="hidden" name="newspack_checkout" value="1" />
<input type="hidden" name="modal_checkout" value="1" />
<input type="hidden" name="product_id" value="<?php echo esc_attr( $product->get_id() ); ?>" />
<button type="submit" class="newspack-ui__button newspack-ui__button--x-small <?php echo esc_attr( $button_class ); ?>"><?php echo esc_html( $button_label ); ?></button>
</form>
</div>
<?php
}

/**
* Print the countdown banner.
*
* @return void
*/
public static function print_cta() {
if ( ! self::is_enabled() ) {
return;
}
$settings = self::get_settings();
$style_class = sprintf( 'is-style-%s', $settings['style'] );
$classes = [ $style_class ];
$total_views = Metering::get_total_metered_views( \is_user_logged_in() );
if ( false === $total_views ) {
return;
}
$views = Metering::get_current_user_metered_views();
if ( $views === 0 || Metering::is_frontend_metering() ) {
$classes[] = 'newspack-countdown-banner__cta--hidden';
}
$metering_settings = Metering::get_metering_settings( Content_Gate::get_gate_post_id() );
$registered_count = $metering_settings['registered_count'];
?>
<div class="newspack-ui">
<div class="banner newspack-countdown-banner__cta <?php echo esc_attr( implode( ' ', $classes ) ); ?>">
<div class="wrapper newspack-countdown-banner__cta__content">
<div class="newspack-countdown-banner__cta__content__wrapper">
<span class="newspack-countdown-banner__cta__content__countdown newspack-ui__font--s">
<strong>
<?php
echo wp_kses_post(
/**
* Filter the countdown message that shows how many metered articles the user has viewed.
* Sanitized via wp_kses_post, so basic HTML is allowed.
*
* @param string $message The countdown message HTML string.
* @param int $views The current number of metered views.
* @param int $total_views The total number of allowed views per period.
* @param string $metering_period The metering period.
* @return string The filtered countdown message HTML string.
*/
apply_filters(
'newspack_countdown_banner_countdown_message',
sprintf(
/* translators: 1: current number of metered views, 2: total metered views, 3: the metering period. */
__( '<span class="newspack-countdown-banner__views">%1$d</span>/<span class="newspack-countdown-banner__total_views">%2$d</span> free articles this %3$s', 'newspack-plugin' ),
$views,
$total_views,
Metering::get_metering_period()
),
$views,
$total_views,
Metering::get_metering_period()
)
);
?>
</strong>
</span>
<span class="newspack-countdown-banner__cta__content__message newspack-ui__font--xs">
<?php echo esc_html( $settings['cta_label'] ); ?>
<?php if ( ! \is_user_logged_in() ) : ?>
<?php if ( $registered_count > 0 ) : ?>
<a href="#register_modal"><?php echo esc_html( __( 'Create an account', 'newspack-plugin' ) ); ?></a>.
<?php else : ?>
<a href="#signin_modal"><?php echo esc_html( __( 'Sign in to an existing account', 'newspack-plugin' ) ); ?></a>.
<?php endif; ?>
<?php endif; ?>
</span>
</div>
<?php self::print_subscribe_button(); ?>
</div>
</div>
</div>
<?php
}

/**
* Filter the body class.
*
* @param array $classes The body classes.
*
* @return array The filtered body classes.
*/
public static function filter_body_class( $classes ) {
if ( self::is_enabled() ) {
$classes[] = 'newspack-has-countdown-banner';
}
return $classes;
}

/**
* Disable the sticky footer ad placement when rendering the countdown banner.
*
* @param array $data The ads placement data.
* @param string $placement_key The placement key.
*
* @return array The filtered ads placement data.
*/
public static function filter_ads_placement_data( $data, $placement_key ) {
if ( ! self::is_enabled() ) {
return $data;
}
if ( $placement_key === 'sticky' ) {
$data['enabled'] = false;
}
return $data;
}
}
Metering_Countdown::init();
5 changes: 5 additions & 0 deletions includes/content-gate/class-metering.php
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ public static function is_logged_in_metering_allowed( $post_id = null ) {
return false;
}

// Not in checkout modals.
if ( method_exists( 'Newspack_Blocks\Modal_Checkout', 'is_modal_checkout' ) && \Newspack_Blocks\Modal_Checkout::is_modal_checkout() ) {
return false;
}

$gate_post_id = Content_Gate::get_gate_post_id();
$metering = \get_post_meta( $gate_post_id, 'metering', true );
$priority = \get_post_meta( $gate_post_id, 'gate_priority', true );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,13 +445,13 @@ public static function enqueue_assets() {
if ( ! self::can_gift_post() && ! self::is_gifted_post() && ! isset( $_GET[ self::QUERY_ARG ] ) && ! is_admin() && ! is_customize_preview() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
wp_enqueue_style( 'newspack-content-gifting', Newspack::plugin_url() . '/dist/content-gifting.css', [], NEWSPACK_PLUGIN_VERSION );
wp_enqueue_style( 'newspack-content-banner', Newspack::plugin_url() . '/dist/content-banner.css', [], NEWSPACK_PLUGIN_VERSION );

if ( is_singular() ) {
$asset = require_once dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/content-gifting.asset.php';
wp_enqueue_script( 'newspack-content-gifting', Newspack::plugin_url() . '/dist/content-gifting.js', $asset['dependencies'], NEWSPACK_PLUGIN_VERSION, true );
$asset = require_once dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/content-banner.asset.php';
wp_enqueue_script( 'newspack-content-banner', Newspack::plugin_url() . '/dist/content-banner.js', $asset['dependencies'], NEWSPACK_PLUGIN_VERSION, true );
wp_localize_script(
'newspack-content-gifting',
'newspack-content-banner',
'newspack_content_gifting',
[
'ajax_url' => add_query_arg(
Expand Down
4 changes: 4 additions & 0 deletions includes/wizards/audience/class-audience-wizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,9 @@ public function api_update_content_gating_settings( $request ) {
if ( isset( $args['show_on_subscription_tab'] ) ) {
Memberships::set_show_on_subscription_tab_setting( (bool) $args['show_on_subscription_tab'] );
}
if ( isset( $args['countdown_banner'] ) ) {
Metering_Countdown::update_settings( $args['countdown_banner'] );
}
if ( isset( $args['content_gifting'] ) ) {
if ( isset( $args['content_gifting']['enabled'] ) ) {
Content_Gifting::set_enabled( (bool) $args['content_gifting']['enabled'] );
Expand Down Expand Up @@ -913,6 +916,7 @@ private static function get_memberships_settings() {
'plans' => Memberships::get_plans(),
'require_all_plans' => Memberships::get_require_all_plans_setting(),
'show_on_subscription_tab' => Memberships::get_show_on_subscription_tab_setting(),
'countdown_banner' => Metering_Countdown::get_settings(),
'content_gifting' => Content_Gifting::get_settings(),
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@
import domReady from '@wordpress/dom-ready';
import { queuePageReload } from '../reader-activation/utils';

import './content-gifting.scss';
import './content-banner.scss';

const settings = window.newspack_metering_settings || {};
const storeKey = 'metering-' + settings.gate_id || 0;

window.newspackRAS = window.newspackRAS || [];

domReady( () => {
const cta = document.querySelector( '.newspack-content-gifting__cta,.newspack-countdown-banner__cta' );
const setBodyOffset = () => {
const cta = document.querySelector( '.newspack-content-gifting__cta' );
if ( ! cta || ! document.body.classList.contains( 'newspack-is-gifted-post' ) ) {
if (
! cta ||
( ! document.body.classList.contains( 'newspack-is-gifted-post' ) &&
! document.body.classList.contains( 'newspack-has-countdown-banner' ) )
) {
return;
}

const updateOffset = () => {
document.body.style.setProperty( '--newspack-content-gifting-cta-offset', `${ cta.offsetHeight }px` );
document.body.style.setProperty( '--newspack-content-banner-cta-offset', `${ cta.offsetHeight }px` );
};

updateOffset();
Expand All @@ -29,6 +36,22 @@ domReady( () => {

setBodyOffset();

// Countdown banner.
window.newspackRAS?.push( ras => {
const views = document.querySelector( '.newspack-countdown-banner__views' );
if ( ! views || '0' !== views.textContent ) {
return;
}
const data = ras?.store?.get( storeKey ) || {
content: [],
};
if ( data.content.length > 0 ) {
views.textContent = data.content.length;
cta.classList.remove( 'newspack-countdown-banner__cta--hidden' );
}
} );

// Content gifting modal.
const modal = document.getElementById( 'newspack-content-gifting-modal' );
if ( ! modal ) {
return;
Expand Down
Loading