Skip to content

Commit c9a68cc

Browse files
dkoothomasguillot
andauthored
feat: metered content countdown banner (#4315)
* feat: move editor files to subfolder; register countdown meta * feat: countdown banner settings UI * style: improve styles for color palette control * feat: migrate settings UI to Audience wizard * feat: reconcile with content gifting feature; implement front-end * fix: recover from no newspackRAS; no render in modal checkout * feat: update light banner style * fix: deprecate ColorPaletteControls due to dependency on block editor * style: match CTA message font size to preview * feat: filter the countdown message * fix: unwanted localized data change * fix: feedback from code reviews * fix: show "create account" link if metering allows registered views * fix: only show signin/register link if not logged in --------- Co-authored-by: Thomas Guillot <thomas@automattic.com>
1 parent 2b15dd3 commit c9a68cc

File tree

17 files changed

+742
-275
lines changed

17 files changed

+742
-275
lines changed

includes/content-gate/class-content-gate.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public static function init() {
7171
include __DIR__ . '/class-content-restriction-control.php';
7272
include __DIR__ . '/class-block-patterns.php';
7373
include __DIR__ . '/class-metering.php';
74+
include __DIR__ . '/class-metering-countdown.php';
7475
include __DIR__ . '/content-gifting/class-content-gifting.php';
7576
}
7677

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
<?php
2+
/**
3+
* WooCommerce Content Gate metering countdown banner.
4+
*
5+
* @package Newspack
6+
*/
7+
8+
namespace Newspack;
9+
10+
/**
11+
* WooCommerce Content Gate metering countdown banner class.
12+
*/
13+
class Metering_Countdown {
14+
15+
const OPTION_PREFIX = 'np_countdown_banner_';
16+
17+
/**
18+
* Initialize hooks.
19+
*/
20+
public static function init() {
21+
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
22+
add_action( 'wp_footer', [ __CLASS__, 'print_cta' ] );
23+
add_filter( 'body_class', [ __CLASS__, 'filter_body_class' ] );
24+
add_filter( 'newspack_ads_placement_data', [ __CLASS__, 'filter_ads_placement_data' ], 10, 2 );
25+
}
26+
27+
/**
28+
* Get all settings with default values for the countdown banner.
29+
*
30+
* @return array Default countdown settings.
31+
*/
32+
public static function get_default_settings() {
33+
return [
34+
'enabled' => false,
35+
'style' => 'light',
36+
'cta_label' => __( 'Subscribe now and get unlimited access.', 'newspack-plugin' ),
37+
'button_label' => __( 'Subscribe now', 'newspack-plugin' ),
38+
'cta_url' => '',
39+
];
40+
}
41+
42+
/**
43+
* Get all settings for the countdown banner.
44+
*
45+
* @param string $key Optional key to get a specific setting. If not provided, all settings will be returned.
46+
*
47+
* @return array|mixed Countdown banner settings, or a specific setting if a key is provided.
48+
*/
49+
public static function get_settings( $key = null ) {
50+
$settings = self::get_default_settings();
51+
if ( $key && isset( $settings[ $key ] ) ) {
52+
return get_option( self::OPTION_PREFIX . $key, $settings[ $key ] );
53+
}
54+
foreach ( $settings as $key => $value ) {
55+
$settings[ $key ] = get_option( self::OPTION_PREFIX . $key, $value );
56+
}
57+
return $settings;
58+
}
59+
60+
/**
61+
* Sanitize a setting.
62+
*
63+
* @param string $key The setting key.
64+
* @param mixed $value The setting value.
65+
*
66+
* @return mixed The sanitized setting value or WP_Error if setting key is invalid.
67+
*/
68+
public static function sanitize_setting( $key, $value ) {
69+
$default_settings = self::get_default_settings();
70+
if ( ! isset( $default_settings[ $key ] ) ) {
71+
// translators: %s is the setting key.
72+
return new \WP_Error( 'newspack_countdown_banner_invalid_setting', sprintf( __( 'Invalid setting key: %s.', 'newspack-plugin' ), $key ) );
73+
}
74+
if ( $key === 'style' && ! in_array( $value, [ 'light', 'dark' ], true ) ) {
75+
return $default_settings[ $key ];
76+
}
77+
if ( $key === 'cta_url' ) {
78+
return sanitize_url( $value );
79+
}
80+
if ( is_bool( $default_settings[ $key ] ) ) {
81+
return boolval( $value );
82+
}
83+
return sanitize_text_field( $value );
84+
}
85+
86+
/**
87+
* Update settings for the countdown banner.
88+
*
89+
* @param array $settings New countdown settings.
90+
*
91+
* @return array|\WP_Error Updated countdown settings or error if update fails.
92+
*/
93+
public static function update_settings( $settings ) {
94+
$current_settings = self::get_settings();
95+
foreach ( $settings as $key => $value ) {
96+
$sanitized = self::sanitize_setting( $key, $value );
97+
if ( is_wp_error( $sanitized ) ) {
98+
return $sanitized;
99+
}
100+
if ( $sanitized === $current_settings[ $key ] ) {
101+
continue;
102+
}
103+
$updated = update_option( self::OPTION_PREFIX . $key, $sanitized );
104+
if ( ! $updated ) {
105+
return new \WP_Error( 'newspack_countdown_banner_update_failed', __( 'Failed to update countdown banner settings.', 'newspack-plugin' ) );
106+
}
107+
$current_settings[ $key ] = $sanitized;
108+
}
109+
return $current_settings;
110+
}
111+
112+
/**
113+
* Whether the countdown banner should be displayed.
114+
*
115+
* @return bool
116+
*/
117+
public static function is_enabled() {
118+
return self::get_settings( 'enabled' ) && Metering::is_metering() && is_singular();
119+
}
120+
121+
/**
122+
* Enqueue assets.
123+
*/
124+
public static function enqueue_assets() {
125+
// Enqueue assets only if enabled and the post is metered.
126+
if ( ! self::is_enabled() ) {
127+
return;
128+
}
129+
$asset = require_once dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/content-banner.asset.php';
130+
131+
// Ensure the content gate metering script is enqueued first.
132+
$asset['dependencies'][] = 'newspack-content-gate-metering';
133+
wp_enqueue_script( 'newspack-content-banner', Newspack::plugin_url() . '/dist/content-banner.js', $asset['dependencies'], NEWSPACK_PLUGIN_VERSION, true );
134+
wp_enqueue_style( 'newspack-content-banner', Newspack::plugin_url() . '/dist/content-banner.css', [], NEWSPACK_PLUGIN_VERSION );
135+
}
136+
137+
/**
138+
* Print the subscribe button.
139+
*/
140+
public static function print_subscribe_button() {
141+
if ( ! class_exists( 'Newspack_Blocks' ) || ! class_exists( 'Newspack_Blocks\Modal_Checkout' ) || ! class_exists( 'Newspack_Blocks\Modal_Checkout\Checkout_Data' ) || ! function_exists( 'wc_get_product' ) ) {
142+
return;
143+
}
144+
$settings = self::get_settings();
145+
$button_label = $settings['button_label'];
146+
$button_class = 'dark' === $settings['style'] ? 'newspack-ui__button--primary-light' : 'newspack-ui__button--accent';
147+
148+
$cta_url = $settings['cta_url'];
149+
if ( $cta_url ) {
150+
?>
151+
<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>
152+
<?php
153+
return;
154+
}
155+
156+
// If CTA url is not provided, try a modal checkout using the primary subscription tier product.
157+
$product = Subscriptions_Tiers::get_primary_subscription_tier_product();
158+
if ( ! $product ) {
159+
return;
160+
}
161+
\Newspack_Blocks\Modal_Checkout::enqueue_modal( $product->get_id() );
162+
\Newspack_Blocks::enqueue_view_assets( 'checkout-button' );
163+
$checkout_data = \Newspack_Blocks\Modal_Checkout\Checkout_Data::get_checkout_data( $product );
164+
?>
165+
<div class="wp-block-newspack-blocks-checkout-button">
166+
<form data-checkout="<?php echo esc_attr( wp_json_encode( $checkout_data ) ); ?>" target="newspack_modal_checkout_iframe">
167+
<input type="hidden" name="newspack_checkout" value="1" />
168+
<input type="hidden" name="modal_checkout" value="1" />
169+
<input type="hidden" name="product_id" value="<?php echo esc_attr( $product->get_id() ); ?>" />
170+
<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>
171+
</form>
172+
</div>
173+
<?php
174+
}
175+
176+
/**
177+
* Print the countdown banner.
178+
*
179+
* @return void
180+
*/
181+
public static function print_cta() {
182+
if ( ! self::is_enabled() ) {
183+
return;
184+
}
185+
$settings = self::get_settings();
186+
$style_class = sprintf( 'is-style-%s', $settings['style'] );
187+
$classes = [ $style_class ];
188+
$total_views = Metering::get_total_metered_views( \is_user_logged_in() );
189+
if ( false === $total_views ) {
190+
return;
191+
}
192+
$views = Metering::get_current_user_metered_views();
193+
if ( $views === 0 || Metering::is_frontend_metering() ) {
194+
$classes[] = 'newspack-countdown-banner__cta--hidden';
195+
}
196+
$metering_settings = Metering::get_metering_settings( Content_Gate::get_gate_post_id() );
197+
$registered_count = $metering_settings['registered_count'];
198+
?>
199+
<div class="newspack-ui">
200+
<div class="banner newspack-countdown-banner__cta <?php echo esc_attr( implode( ' ', $classes ) ); ?>">
201+
<div class="wrapper newspack-countdown-banner__cta__content">
202+
<div class="newspack-countdown-banner__cta__content__wrapper">
203+
<span class="newspack-countdown-banner__cta__content__countdown newspack-ui__font--s">
204+
<strong>
205+
<?php
206+
echo wp_kses_post(
207+
/**
208+
* Filter the countdown message that shows how many metered articles the user has viewed.
209+
* Sanitized via wp_kses_post, so basic HTML is allowed.
210+
*
211+
* @param string $message The countdown message HTML string.
212+
* @param int $views The current number of metered views.
213+
* @param int $total_views The total number of allowed views per period.
214+
* @param string $metering_period The metering period.
215+
* @return string The filtered countdown message HTML string.
216+
*/
217+
apply_filters(
218+
'newspack_countdown_banner_countdown_message',
219+
sprintf(
220+
/* translators: 1: current number of metered views, 2: total metered views, 3: the metering period. */
221+
__( '<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' ),
222+
$views,
223+
$total_views,
224+
Metering::get_metering_period()
225+
),
226+
$views,
227+
$total_views,
228+
Metering::get_metering_period()
229+
)
230+
);
231+
?>
232+
</strong>
233+
</span>
234+
<span class="newspack-countdown-banner__cta__content__message newspack-ui__font--xs">
235+
<?php echo esc_html( $settings['cta_label'] ); ?>
236+
<?php if ( ! \is_user_logged_in() ) : ?>
237+
<?php if ( $registered_count > 0 ) : ?>
238+
<a href="#register_modal"><?php echo esc_html( __( 'Create an account', 'newspack-plugin' ) ); ?></a>.
239+
<?php else : ?>
240+
<a href="#signin_modal"><?php echo esc_html( __( 'Sign in to an existing account', 'newspack-plugin' ) ); ?></a>.
241+
<?php endif; ?>
242+
<?php endif; ?>
243+
</span>
244+
</div>
245+
<?php self::print_subscribe_button(); ?>
246+
</div>
247+
</div>
248+
</div>
249+
<?php
250+
}
251+
252+
/**
253+
* Filter the body class.
254+
*
255+
* @param array $classes The body classes.
256+
*
257+
* @return array The filtered body classes.
258+
*/
259+
public static function filter_body_class( $classes ) {
260+
if ( self::is_enabled() ) {
261+
$classes[] = 'newspack-has-countdown-banner';
262+
}
263+
return $classes;
264+
}
265+
266+
/**
267+
* Disable the sticky footer ad placement when rendering the countdown banner.
268+
*
269+
* @param array $data The ads placement data.
270+
* @param string $placement_key The placement key.
271+
*
272+
* @return array The filtered ads placement data.
273+
*/
274+
public static function filter_ads_placement_data( $data, $placement_key ) {
275+
if ( ! self::is_enabled() ) {
276+
return $data;
277+
}
278+
if ( $placement_key === 'sticky' ) {
279+
$data['enabled'] = false;
280+
}
281+
return $data;
282+
}
283+
}
284+
Metering_Countdown::init();

includes/content-gate/class-metering.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ public static function is_logged_in_metering_allowed( $post_id = null ) {
273273
return false;
274274
}
275275

276+
// Not in checkout modals.
277+
if ( method_exists( 'Newspack_Blocks\Modal_Checkout', 'is_modal_checkout' ) && \Newspack_Blocks\Modal_Checkout::is_modal_checkout() ) {
278+
return false;
279+
}
280+
276281
$gate_post_id = Content_Gate::get_gate_post_id();
277282
$metering = \get_post_meta( $gate_post_id, 'metering', true );
278283
$priority = \get_post_meta( $gate_post_id, 'gate_priority', true );

includes/content-gate/content-gifting/class-content-gifting.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -445,13 +445,13 @@ public static function enqueue_assets() {
445445
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
446446
return;
447447
}
448-
wp_enqueue_style( 'newspack-content-gifting', Newspack::plugin_url() . '/dist/content-gifting.css', [], NEWSPACK_PLUGIN_VERSION );
448+
wp_enqueue_style( 'newspack-content-banner', Newspack::plugin_url() . '/dist/content-banner.css', [], NEWSPACK_PLUGIN_VERSION );
449449

450450
if ( is_singular() ) {
451-
$asset = require_once dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/content-gifting.asset.php';
452-
wp_enqueue_script( 'newspack-content-gifting', Newspack::plugin_url() . '/dist/content-gifting.js', $asset['dependencies'], NEWSPACK_PLUGIN_VERSION, true );
451+
$asset = require_once dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/content-banner.asset.php';
452+
wp_enqueue_script( 'newspack-content-banner', Newspack::plugin_url() . '/dist/content-banner.js', $asset['dependencies'], NEWSPACK_PLUGIN_VERSION, true );
453453
wp_localize_script(
454-
'newspack-content-gifting',
454+
'newspack-content-banner',
455455
'newspack_content_gifting',
456456
[
457457
'ajax_url' => add_query_arg(

includes/wizards/audience/class-audience-wizard.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,9 @@ public function api_update_content_gating_settings( $request ) {
626626
if ( isset( $args['show_on_subscription_tab'] ) ) {
627627
Memberships::set_show_on_subscription_tab_setting( (bool) $args['show_on_subscription_tab'] );
628628
}
629+
if ( isset( $args['countdown_banner'] ) ) {
630+
Metering_Countdown::update_settings( $args['countdown_banner'] );
631+
}
629632
if ( isset( $args['content_gifting'] ) ) {
630633
if ( isset( $args['content_gifting']['enabled'] ) ) {
631634
Content_Gifting::set_enabled( (bool) $args['content_gifting']['enabled'] );
@@ -913,6 +916,7 @@ private static function get_memberships_settings() {
913916
'plans' => Memberships::get_plans(),
914917
'require_all_plans' => Memberships::get_require_all_plans_setting(),
915918
'show_on_subscription_tab' => Memberships::get_show_on_subscription_tab_setting(),
919+
'countdown_banner' => Metering_Countdown::get_settings(),
916920
'content_gifting' => Content_Gifting::get_settings(),
917921
];
918922
}
Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,26 @@
22
import domReady from '@wordpress/dom-ready';
33
import { queuePageReload } from '../reader-activation/utils';
44

5-
import './content-gifting.scss';
5+
import './content-banner.scss';
6+
7+
const settings = window.newspack_metering_settings || {};
8+
const storeKey = 'metering-' + settings.gate_id || 0;
69

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

912
domReady( () => {
13+
const cta = document.querySelector( '.newspack-content-gifting__cta,.newspack-countdown-banner__cta' );
1014
const setBodyOffset = () => {
11-
const cta = document.querySelector( '.newspack-content-gifting__cta' );
12-
if ( ! cta || ! document.body.classList.contains( 'newspack-is-gifted-post' ) ) {
15+
if (
16+
! cta ||
17+
( ! document.body.classList.contains( 'newspack-is-gifted-post' ) &&
18+
! document.body.classList.contains( 'newspack-has-countdown-banner' ) )
19+
) {
1320
return;
1421
}
1522

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

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

3037
setBodyOffset();
3138

39+
// Countdown banner.
40+
window.newspackRAS?.push( ras => {
41+
const views = document.querySelector( '.newspack-countdown-banner__views' );
42+
if ( ! views || '0' !== views.textContent ) {
43+
return;
44+
}
45+
const data = ras?.store?.get( storeKey ) || {
46+
content: [],
47+
};
48+
if ( data.content.length > 0 ) {
49+
views.textContent = data.content.length;
50+
cta.classList.remove( 'newspack-countdown-banner__cta--hidden' );
51+
}
52+
} );
53+
54+
// Content gifting modal.
3255
const modal = document.getElementById( 'newspack-content-gifting-modal' );
3356
if ( ! modal ) {
3457
return;

0 commit comments

Comments
 (0)