-
Notifications
You must be signed in to change notification settings - Fork 58
feat: metered content countdown banner #4315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
8089ec5
feat: move editor files to subfolder; register countdown meta
dkoo 01f1d5a
feat: countdown banner settings UI
dkoo f60cfdf
style: improve styles for color palette control
dkoo c259ee0
Merge branch 'trunk' into feat/content-gate-meter-countdown
dkoo cee8704
feat: migrate settings UI to Audience wizard
dkoo ef1f2ad
Merge branch 'trunk' into feat/content-gate-meter-countdown
dkoo 9eb9155
feat: reconcile with content gifting feature; implement front-end
dkoo e559cc4
fix: recover from no newspackRAS; no render in modal checkout
dkoo 43b9e0c
feat: update light banner style
thomasguillot 02a8f6a
fix: deprecate ColorPaletteControls due to dependency on block editor
dkoo 10cf115
style: match CTA message font size to preview
dkoo 4ee8656
Merge branch 'trunk' into feat/content-gate-meter-countdown
dkoo a02a3d2
feat: filter the countdown message
dkoo 752545e
fix: unwanted localized data change
dkoo e792bc5
Merge branch 'trunk' into feat/content-gate-meter-countdown
dkoo 69be0b9
Merge branch 'trunk' into feat/content-gate-meter-countdown
dkoo dbb794b
fix: feedback from code reviews
dkoo 5a32890
fix: show "create account" link if metering allows registered views
dkoo 224abbb
fix: only show signin/register link if not logged in
dkoo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
| */ | ||
| 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; | ||
| } | ||
dkoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Metering_Countdown::init(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
@returndocumentation states it returnsarray|\WP_Errorbut the function never returns a\WP_Error. It always returns the$current_settingsarray. Either the documentation should be updated to only indicate@return array, or error handling should be implemented to actually return a\WP_Errorwhen appropriate.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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