diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a4b5ef260..aa18d1b61d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -456,7 +456,6 @@ ress/* packages # [4.4.0](https://github.com/Automattic/newspack-plugin/compare/v4.3.4...v4.4.0) (2024-07-01) - ### Bug Fixes * add modified check before updating donation product ([#3183](https://github.com/Automattic/newspack-plugin/issues/3183)) ([208c55e](https://github.com/Automattic/newspack-plugin/commit/208c55e21ac8f6a4b6736f89c25cf12994f2cbaf)) @@ -563,6 +562,13 @@ ress/* packages * **ia:** back to `trunk` ([69b2ba0](https://github.com/Automattic/newspack-plugin/commit/69b2ba09a222e7c1b84b9cba0b97c36881cda63f)) +## [4.3.4](https://github.com/Automattic/newspack-plugin/compare/v4.3.3...v4.3.4) (2024-06-27) + +### Bug Fixes + +* variable name > constant ([#3203](https://github.com/Automattic/newspack-plugin/issues/3203)) ([46c5651](https://github.com/Automattic/newspack-plugin/commit/46c5651cf48e88abef3b8f3855b8fd3f5860c2a3)) + + ## [4.3.3](https://github.com/Automattic/newspack-plugin/compare/v4.3.2...v4.3.3) (2024-06-24) diff --git a/includes/class-blocks.php b/includes/class-blocks.php index eccc0fafa8..a315a121af 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -42,7 +42,7 @@ public static function enqueue_block_editor_assets() { 'has_reader_activation' => Reader_Activation::is_enabled(), 'newsletters_url' => Wizards::get_wizard( 'engagement' )->newsletters_settings_url(), 'has_google_oauth' => Google_OAuth::is_oauth_configured(), - 'google_logo_svg' => file_get_contents( dirname( NEWSPACK_PLUGIN_FILE ) . '/src/blocks/reader-registration/icons/google.svg' ), + '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(), diff --git a/includes/class-donations.php b/includes/class-donations.php index 54d21e369a..06c2b44a38 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -64,18 +64,24 @@ class Donations { * @codeCoverageIgnore */ public static function init() { - self::$donation_product_name = __( 'Donate', 'newspack' ); - if ( ! is_admin() ) { - add_action( 'wp_loaded', [ __CLASS__, 'process_donation_form' ], 99 ); - add_action( 'woocommerce_checkout_update_order_meta', [ __CLASS__, 'woocommerce_checkout_update_order_meta' ] ); - add_filter( 'woocommerce_billing_fields', [ __CLASS__, 'woocommerce_billing_fields' ] ); - add_filter( 'pre_option_woocommerce_enable_guest_checkout', [ __CLASS__, 'disable_guest_checkout' ] ); - add_action( 'woocommerce_check_cart_items', [ __CLASS__, 'handle_cart' ] ); - add_filter( 'amp_skip_post', [ __CLASS__, 'should_skip_amp' ], 10, 2 ); - add_filter( 'newspack_blocks_donate_billing_fields_keys', [ __CLASS__, 'get_billing_fields' ] ); - add_action( 'woocommerce_checkout_create_order_line_item', [ __CLASS__, 'checkout_create_order_line_item' ], 10, 4 ); - add_action( 'woocommerce_coupons_enabled', [ __CLASS__, 'disable_coupons' ] ); - } + self::$donation_product_name = __( 'Donate', 'newspack-plugin' ); + + // Process donation request. + add_action( 'wp_ajax_modal_checkout_request', [ __CLASS__, 'process_donation_request' ] ); + add_action( 'wp_ajax_nopriv_modal_checkout_request', [ __CLASS__, 'process_donation_request' ] ); + add_action( 'wp_loaded', [ __CLASS__, 'process_donation_request' ], 99 ); + + add_action( 'woocommerce_checkout_update_order_meta', [ __CLASS__, 'woocommerce_checkout_update_order_meta' ] ); + add_filter( 'woocommerce_billing_fields', [ __CLASS__, 'woocommerce_billing_fields' ] ); + add_filter( 'pre_option_woocommerce_enable_guest_checkout', [ __CLASS__, 'disable_guest_checkout' ] ); + add_action( 'woocommerce_check_cart_items', [ __CLASS__, 'handle_cart' ] ); + add_filter( 'amp_skip_post', [ __CLASS__, 'should_skip_amp' ], 10, 2 ); + add_filter( 'newspack_blocks_donate_billing_fields_keys', [ __CLASS__, 'get_billing_fields' ] ); + add_action( 'woocommerce_checkout_create_order_line_item', [ __CLASS__, 'checkout_create_order_line_item' ], 10, 4 ); + add_filter( 'woocommerce_coupons_enabled', [ __CLASS__, 'disable_coupons' ] ); + add_filter( 'wcs_place_subscription_order_text', [ __CLASS__, 'order_button_text' ], 9 ); + add_filter( 'woocommerce_order_button_text', [ __CLASS__, 'order_button_text' ], 9 ); + add_filter( 'option_woocommerce_subscriptions_order_button_text', [ __CLASS__, 'order_button_text' ], 9 ); } /** @@ -647,7 +653,11 @@ public static function is_platform_other() { /** * Handle submission of the donation form. */ - public static function process_donation_form() { + public static function process_donation_request() { + if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { + return; + } + $is_wc = self::is_platform_wc(); $donation_form_submitted = filter_input( INPUT_GET, 'newspack_donate', FILTER_SANITIZE_NUMBER_INT ); @@ -737,6 +747,11 @@ function ( $item ) { [], $cart_item_data ); + + // Set checkout registration flag if user is not logged in. + if ( ! is_user_logged_in() && class_exists( '\Newspack_Blocks\Modal_Checkout' ) ) { + \Newspack_Blocks\Modal_Checkout::set_checkout_registration_flag(); + } } $query_args = []; @@ -771,8 +786,16 @@ function ( $item ) { ); // Redirect to checkout. - \wp_safe_redirect( apply_filters( 'newspack_donation_checkout_url', $checkout_url, $donation_value, $donation_frequency ) ); - exit; + $checkout_url = apply_filters( 'newspack_donation_checkout_url', $checkout_url, $donation_value, $donation_frequency ); + + if ( defined( 'DOING_AJAX' ) ) { + echo wp_json_encode( [ 'url' => $checkout_url ] ); + exit; + } else { + // Redirect to checkout. + \wp_safe_redirect( $checkout_url ); + exit; + } } /** @@ -1085,6 +1108,9 @@ public static function update_billing_fields( $billing_fields ) { * @return bool */ public static function disable_coupons( $enabled ) { + if ( is_admin() ) { + return $enabled; + } $cart = WC()->cart; if ( ! $cart ) { return $enabled; @@ -1094,5 +1120,17 @@ public static function disable_coupons( $enabled ) { } return false; } + + /** + * Set the "Place order" button text. + * + * @param string $text The button text. + */ + public static function order_button_text( $text ) { + if ( self::is_donation_cart() ) { + return __( 'Donate now', 'newspack-plugin' ); + } + return $text; + } } Donations::init(); diff --git a/includes/class-magic-link.php b/includes/class-magic-link.php index 9213a408e6..2a36744359 100644 --- a/includes/class-magic-link.php +++ b/includes/class-magic-link.php @@ -21,8 +21,9 @@ final class Magic_Link { 'enable' => 'np_magic_link_enable', ]; - const TOKENS_META = 'np_magic_link_tokens'; - const DISABLED_META = 'np_magic_link_disabled'; + const TOKENS_META = 'np_magic_link_tokens'; + const DISABLED_META = 'np_magic_link_disabled'; + const USER_SECRET_META = 'np_magic_link_secret'; const AUTH_ACTION = 'np_auth_link'; const AUTH_ACTION_RESULT = 'np_auth_link_result'; @@ -34,6 +35,9 @@ final class Magic_Link { const OTP_MAX_ATTEMPTS = 5; const OTP_AUTH_ACTION = 'np_otp_auth'; const OTP_HASH_COOKIE = 'np_otp_hash'; + const ACCEPTED_PARAMS = [ + 'checkout', + ]; /** * Current session secret. @@ -61,6 +65,7 @@ public static function init() { /** Replace Newspack Newsletters Verification Email */ \add_filter( 'newspack_newsletters_email_verification_email', [ __CLASS__, 'newsletters_email_verification_email' ], 10, 3 ); + \add_action( 'wp_logout', [ __CLASS__, 'clear_user_tokens' ], 10, 1 ); } /** @@ -255,10 +260,11 @@ private static function get_client_hash( $user, $reset_secret = false ) { /** * Clear all user tokens. * - * @param \WP_User $user User to clear tokens for. + * @param \WP_User|int $user User or user ID to clear tokens for. */ public static function clear_user_tokens( $user ) { - \delete_user_meta( $user->ID, self::TOKENS_META ); + $user_id = $user instanceof \WP_User ? $user->ID : $user; + \delete_user_meta( $user_id, self::TOKENS_META ); /** * Fires after all user tokens are cleared. @@ -334,7 +340,7 @@ private static function generate_otp( $user ) { */ public static function generate_token( $user ) { if ( ! self::can_magic_link( $user->ID ) ) { - return new \WP_Error( 'newspack_magic_link_invalid_user', __( 'Invalid user.', 'newspack' ) ); + return new \WP_Error( 'newspack_magic_link_invalid_user', __( 'Invalid user.', 'newspack-plugin' ) ); } $now = time(); @@ -345,6 +351,14 @@ public static function generate_token( $user ) { $expire = $now - self::get_token_expiration_period(); if ( ! empty( $tokens ) ) { + + /** + * Filters the magic link rate interval. + * + * @param int $rate_interval Magic link rate interval. + */ + $rate_interval = apply_filters( 'newspack_magic_link_rate_interval', self::RATE_INTERVAL ); + /** Limit maximum tokens to 5. */ $tokens = array_slice( $tokens, -4, 4 ); foreach ( $tokens as $index => $token_data ) { @@ -353,8 +367,8 @@ public static function generate_token( $user ) { unset( $tokens[ $index ] ); } /** Rate limit token generation. */ - if ( $token_data['time'] + self::RATE_INTERVAL > $now ) { - return new \WP_Error( 'rate_limit_exceeded', __( 'Please wait a minute before requesting another authorization code.', 'newspack' ) ); + if ( $token_data['time'] + $rate_interval > $now ) { + return new \WP_Error( 'rate_limit_exceeded', __( 'Please wait a minute before requesting another authorization code.', 'newspack-plugin' ) ); } } $tokens = array_values( $tokens ); @@ -374,6 +388,60 @@ public static function generate_token( $user ) { return $token_data; } + /** + * Generate secret. + * + * @param \WP_User $user User to generate the secret for. + * + * @return string + */ + public static function generate_secret( $user ) { + $secret = \get_user_meta( $user->ID, self::USER_SECRET_META, true ); + if ( ! empty( $secret ) ) { + return $secret; + } + $secret = wp_hash( $user->user_email ); + \update_user_meta( $user->ID, self::USER_SECRET_META, $secret ); + return $secret; + } + + /** + * Check for active magic link tokens. + * + * @param \WP_User $user User to check the active magic link token for. + * + * @return bool|\WP_Error + */ + public static function has_active_token( $user ) { + if ( ! self::can_magic_link( $user->ID ) ) { + return new \WP_Error( 'newspack_magic_link_invalid_user', __( 'Invalid user.', 'newspack-plugin' ) ); + } + + // If an OTP hash cookie is not set, ignore any active tokens. + if ( ! isset( $_COOKIE[ self::OTP_HASH_COOKIE ] ) ) { + return false; + } + + $now = time(); + $tokens = \get_user_meta( $user->ID, self::TOKENS_META, true ); + + $expire = $now - self::get_token_expiration_period(); + if ( ! empty( $tokens ) ) { + foreach ( $tokens as $index => $token_data ) { + /** Clear expired tokens. */ + if ( $token_data['time'] < $expire ) { + unset( $tokens[ $index ] ); + } + } + } + + if ( empty( $tokens ) ) { + return false; + } + + return true; + } + /** * Generate a magic link. * @@ -392,8 +460,8 @@ public static function generate_url( $user, $url = '' ) { return \add_query_arg( [ 'action' => self::AUTH_ACTION, - 'email' => urlencode( $user->user_email ), 'token' => $token_data['token'], + 'secret' => self::generate_secret( $user ), ], ! empty( $url ) ? $url : \home_url() ); @@ -413,11 +481,16 @@ public static function send_email( $user, $redirect_to = '', $use_otp = true ) { if ( \is_wp_error( $token_data ) ) { return $token_data; } - $url = \add_query_arg( + + $key = \get_password_reset_key( $user ); + if ( is_wp_error( $key ) ) { + return $key; + } + $url = \add_query_arg( [ 'action' => self::AUTH_ACTION, - 'email' => urlencode( $user->user_email ), 'token' => $token_data['token'], + 'secret' => self::generate_secret( $user ), ], ! empty( $redirect_to ) ? $redirect_to : \home_url() ); @@ -427,6 +500,10 @@ public static function send_email( $user, $redirect_to = '', $use_otp = true ) { 'template' => '*MAGIC_LINK_URL*', 'value' => $url, ], + [ + 'template' => '*SET_PASSWORD_LINK*', + 'value' => Emails::get_password_reset_url( $user, $key ), + ], ]; if ( $use_otp && ! empty( $token_data['otp'] ) ) { $email_type = 'OTP_AUTH'; @@ -466,13 +543,13 @@ public static function validate_token( $user_id, $client, $token ) { $user = \get_user_by( 'id', $user_id ); if ( ! $user ) { - $errors->add( 'invalid_user', __( 'User not found.', 'newspack' ) ); + $errors->add( 'invalid_user', __( 'User not found.', 'newspack-plugin' ) ); } elseif ( ! self::can_magic_link( $user->ID ) ) { - $errors->add( 'invalid_user_type', __( 'Not allowed for this user', 'newspack' ) ); + $errors->add( 'invalid_user_type', __( 'Not allowed for this user', 'newspack-plugin' ) ); } else { $tokens = \get_user_meta( $user->ID, self::TOKENS_META, true ); if ( empty( $tokens ) || empty( $token ) ) { - $errors->add( 'invalid_token', __( 'Invalid token.', 'newspack' ) ); + $errors->add( 'invalid_token', __( 'Invalid token.', 'newspack-plugin' ) ); } } @@ -493,7 +570,7 @@ public static function validate_token( $user_id, $client, $token ) { /** If token data has a client hash, it must be equal to the user's. */ if ( ! empty( $token_data['client'] ) && $token_data['client'] !== $client ) { - $errors->add( 'invalid_client', __( 'Invalid client.', 'newspack' ) ); + $errors->add( 'invalid_client', __( 'Invalid client.', 'newspack-plugin' ) ); } unset( $tokens[ $index ] ); @@ -502,7 +579,7 @@ public static function validate_token( $user_id, $client, $token ) { } if ( empty( $valid_token ) ) { - $errors->add( 'invalid_token', __( 'Invalid token.', 'newspack' ) ); + $errors->add( 'invalid_token', __( 'Invalid token.', 'newspack-plugin' ) ); } self::clear_token_cookies(); @@ -536,17 +613,17 @@ public static function validate_otp( $user_id, $hash, $code ) { $user = \get_user_by( 'id', $user_id ); if ( ! $user ) { - $errors->add( 'invalid_user', __( 'User not found.', 'newspack' ) ); + $errors->add( 'invalid_user', __( 'User not found.', 'newspack-plugin' ) ); } elseif ( ! self::can_magic_link( $user->ID ) ) { - $errors->add( 'invalid_user_type', __( 'Not allowed for this user', 'newspack' ) ); + $errors->add( 'invalid_user_type', __( 'Not allowed for this user', 'newspack-plugin' ) ); } elseif ( ! self::is_otp_enabled( $user ) ) { - $errors->add( 'invalid_otp', __( 'OTP is not enabled.', 'newspack' ) ); + $errors->add( 'invalid_otp', __( 'OTP is not enabled.', 'newspack-plugin' ) ); } else { $tokens = \get_user_meta( $user->ID, self::TOKENS_META, true ); if ( empty( $tokens ) || empty( $hash ) ) { - $errors->add( 'invalid_hash', __( 'Invalid hash.', 'newspack' ) ); + $errors->add( 'invalid_hash', __( 'Invalid hash.', 'newspack-plugin' ) ); } elseif ( empty( $code ) ) { - $errors->add( 'invalid_otp', __( 'Invalid OTP.', 'newspack' ) ); + $errors->add( 'invalid_otp', __( 'Invalid OTP.', 'newspack-plugin' ) ); } } @@ -572,18 +649,18 @@ public static function validate_otp( $user_id, $hash, $code ) { // Handle OTP attempts from given hash. $tokens[ $index ]['otp']['attempts']++; if ( $token_data['otp']['attempts'] >= self::OTP_MAX_ATTEMPTS ) { - $errors->add( 'max_otp_attempts', __( 'Maximum OTP attempts reached.', 'newspack' ) ); + $errors->add( 'max_otp_attempts', __( 'Maximum OTP attempts reached.', 'newspack-plugin' ) ); unset( $tokens[ $index ] ); self::clear_token_cookies(); } - $errors->add( 'invalid_otp', __( 'Invalid OTP.', 'newspack' ) ); + $errors->add( 'invalid_otp', __( 'Invalid OTP.', 'newspack-plugin' ) ); } break; } } if ( empty( $valid_token ) ) { - $errors->add( 'invalid_hash', __( 'Invalid hash.', 'newspack' ) ); + $errors->add( 'invalid_hash', __( 'Invalid hash.', 'newspack-plugin' ) ); } $tokens = array_values( $tokens ); @@ -619,12 +696,12 @@ private static function authenticate( $user_id, $token_or_otp_hash, $otp_code = $user = \get_user_by( 'id', $user_id ); if ( ! $user ) { - return new \WP_Error( 'invalid_user', __( 'User not found.', 'newspack' ) ); + return new \WP_Error( 'invalid_user', __( 'User not found.', 'newspack-plugin' ) ); } if ( ! empty( $otp_hash ) ) { if ( ! self::is_otp_enabled( $user ) ) { - return new \WP_Error( 'invalid_otp', __( 'OTP is not enabled.', 'newspack' ) ); + return new \WP_Error( 'invalid_otp', __( 'OTP is not enabled.', 'newspack-plugin' ) ); } $token_data = self::validate_otp( $user_id, $otp_hash, $otp_code ); } else { @@ -660,7 +737,6 @@ public static function process_token_request() { if ( ! Reader_Activation::is_enabled() ) { return; } - // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET[ self::AUTH_ACTION_RESULT ] ) && 0 === \absint( $_GET[ self::AUTH_ACTION_RESULT ] ) ) { \add_action( @@ -676,58 +752,64 @@ function () { }
+ | @@ -1031,8 +1113,8 @@ public static function edit_user_profile( $user ) { display_name ) ); ?> @@ -1041,14 +1123,14 @@ public static function edit_user_profile( $user ) { | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
+ |
display_name ), \esc_html( \absint( self::get_token_expiration_period() ) / MINUTE_IN_SECONDS ) ); @@ -1057,14 +1139,14 @@ public static function edit_user_profile( $user ) { |
|||||||||||||
+ |
display_name ) ); ?> @@ -1104,7 +1186,7 @@ public static function newsletters_email_verification_email( $email, $user, $url $verification_url = \add_query_arg( [ 'action' => self::AUTH_ACTION, - 'email' => urlencode( $user->user_email ), + 'secret' => self::generate_secret( $user ), 'token' => $token_data['token'], 'redirect' => urlencode( $url ), ], diff --git a/includes/class-newspack-ui-icons.php b/includes/class-newspack-ui-icons.php new file mode 100644 index 0000000000..75553d3342 --- /dev/null +++ b/includes/class-newspack-ui-icons.php @@ -0,0 +1,114 @@ + array( + 'class' => true, + 'aria-hidden' => true, + 'aria-labelledby' => true, + 'role' => true, + 'xmlns' => true, + 'width' => true, + 'height' => true, + 'viewbox' => true, + ), + 'g' => array( + 'fill' => true, + 'fill-rule' => true, + ), + 'title' => array( + 'title' => true, + ), + 'path' => array( + 'd' => true, + 'fill' => true, + 'fill-rule' => true, + ), + 'defs' => true, + 'clipPath' => true, + 'polygon' => array( + 'points' => true, + ), + ); + + return $svg_args; + } + + /** + * User Interface icons – svg sources. + * + * @var array + */ + public static $ui_icons = array( + 'account' => + '', + 'check' => + '', + 'close' => + '', + 'error' => + '', + 'google' => + '', + 'menu' => + '', + 'info' => + '', + ); +} diff --git a/includes/class-newspack-ui.php b/includes/class-newspack-ui.php new file mode 100644 index 0000000000..8544a892d3 --- /dev/null +++ b/includes/class-newspack-ui.php @@ -0,0 +1,906 @@ + +
+
+ $path ) {
+ if ( $slug !== $plugin_slug && str_contains( $slug, $plugin_slug ) ) {
+ $versions = [
+ 'alpha',
+ 'epic',
+ ];
+
+ foreach ( $versions as $version ) {
+ if ( str_contains( $slug, $version ) && \is_plugin_active( $path ) ) {
+ $status = 'active';
+ break;
+ }
+ }
+ }
+ }
+ }
+
return $status;
}
diff --git a/includes/class-recaptcha.php b/includes/class-recaptcha.php
index 171b42ba86..4d7cbfdf12 100644
--- a/includes/class-recaptcha.php
+++ b/includes/class-recaptcha.php
@@ -7,8 +7,6 @@
namespace Newspack;
-use Error;
-
defined( 'ABSPATH' ) || exit;
/**
@@ -26,11 +24,6 @@ final class Recaptcha {
public static function init() {
\add_action( 'rest_api_init', [ __CLASS__, 'register_api_endpoints' ] );
\add_action( 'wp_enqueue_scripts', [ __CLASS__, 'register_scripts' ] );
- \add_action( 'newspack_newsletters_subscribe_block_before_email_field', [ __CLASS__, 'render_recaptcha_v2_container' ] );
-
- // Add reCAPTCHA to the Woo checkout form.
- \add_action( 'woocommerce_review_order_before_submit', [ __CLASS__, 'add_recaptcha_v2_to_checkout' ] );
- \add_action( 'woocommerce_checkout_after_customer_details', [ __CLASS__, 'add_recaptcha_v3_to_checkout' ] );
// Verify reCAPTCHA on checkout submission.
\add_action( 'woocommerce_checkout_process', [ __CLASS__, 'verify_recaptcha_on_checkout' ] );
@@ -111,17 +104,14 @@ public static function get_script_url() {
* Register the reCAPTCHA script.
*/
public static function register_scripts() {
- // Styles only apply to the visible v2 widgets.
- if ( self::can_use_captcha( 'v2' ) ) {
+ if ( self::can_use_captcha() ) {
\wp_enqueue_style(
self::SCRIPT_HANDLE,
Newspack::plugin_url() . '/dist/other-scripts/recaptcha.css',
[],
NEWSPACK_PLUGIN_VERSION
);
- }
- if ( self::can_use_captcha() ) {
// Enqueue the reCAPTCHA API from Google's servers.
// Note: version arg Must be null to avoid the &ver param being read as part of the reCAPTCHA site key.
\wp_register_script(
@@ -131,18 +121,17 @@ public static function register_scripts() {
null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
false
);
- \wp_script_add_data( self::SCRIPT_HANDLE_API, 'async', true );
- \wp_script_add_data( self::SCRIPT_HANDLE_API, 'defer', true );
\wp_enqueue_script(
self::SCRIPT_HANDLE,
Newspack::plugin_url() . '/dist/other-scripts/recaptcha.js',
- [ self::SCRIPT_HANDLE_API ],
+ [ self::SCRIPT_HANDLE_API, 'wp-i18n' ],
NEWSPACK_PLUGIN_VERSION,
- true
+ [
+ 'strategy' => 'async',
+ 'in_footer' => true,
+ ]
);
- \wp_script_add_data( self::SCRIPT_HANDLE, 'async', true );
- \wp_script_add_data( self::SCRIPT_HANDLE, 'defer', true );
\wp_localize_script(
self::SCRIPT_HANDLE,
@@ -165,7 +154,7 @@ public static function api_permissions_check() {
if ( ! \current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'newspack_rest_forbidden',
- \esc_html__( 'You cannot use this resource.', 'newspack' ),
+ \esc_html__( 'You cannot use this resource.', 'newspack-plugin' ),
[
'status' => 403,
]
@@ -182,8 +171,7 @@ public static function api_permissions_check() {
public static function get_settings_config() {
return [
'use_captcha' => false,
- 'site_key' => '',
- 'site_secret' => '',
+ 'credentials' => [],
'threshold' => 0.5,
'version' => 'v3',
];
@@ -206,7 +194,19 @@ public static function api_get_settings() {
* @return WP_REST_Response containing the settings list.
*/
public static function api_update_settings( $request ) {
- return \rest_ensure_response( self::update_settings( $request->get_params() ) );
+ $params = $request->get_params();
+ if ( isset( $params['site_key'] ) || isset( $params['site_secret'] ) ) {
+ $version = $params['version'] ?? self::get_setting( 'version' );
+ $credentials = self::get_setting( 'credentials' );
+ if ( isset( $params['site_key'] ) ) {
+ $credentials[ $version ]['site_key'] = $params['site_key'];
+ }
+ if ( isset( $params['site_secret'] ) ) {
+ $credentials[ $version ]['site_secret'] = $params['site_secret'];
+ }
+ $params['credentials'] = $credentials;
+ }
+ return \rest_ensure_response( self::update_settings( $params ) );
}
/**
@@ -221,22 +221,32 @@ public static function get_settings() {
$settings[ $key ] = self::get_setting( $key );
}
- // Migrate reCAPTCHA settings from Stripe wizard, for more generalized usage.
- if ( ! $settings['use_captcha'] && empty( $settings['site_key'] ) && empty( $settings['site_secret'] ) ) {
- $stripe_settings = Stripe_Connection::get_stripe_data();
- if ( ! empty( $stripe_settings['useCaptcha'] ) && ! empty( $stripe_settings['captchaSiteKey'] ) && ! empty( $stripe_settings['captchaSiteSecret'] ) ) {
- // If we have all of the required settings in Stripe settings, migrate them here.
- self::update_settings(
- [
- 'use_captcha' => $stripe_settings['useCaptcha'],
- 'site_key' => $stripe_settings['captchaSiteKey'],
- 'site_secret' => $stripe_settings['captchaSiteSecret'],
- ]
- );
-
- $settings['use_captcha'] = $stripe_settings['useCaptcha'];
- $settings['site_key'] = $stripe_settings['captchaSiteKey'];
- $settings['site_secret'] = $stripe_settings['captchaSiteSecret'];
+ // Migrate reCAPTCHA settings from separate site_key/site_secret options to credentials array.
+ $current_version = $settings['version'];
+ if (
+ $settings['use_captcha'] &&
+ (
+ empty( $settings['credentials'][ $current_version ]['site_key'] ) ||
+ empty( $settings['credentials'][ $current_version ]['site_secret'] )
+ )
+ ) {
+ $legacy_key = \get_option( self::OPTIONS_PREFIX . 'site_key', false );
+ $legacy_secret = \get_option( self::OPTIONS_PREFIX . 'site_secret', false );
+
+ if ( ! empty( $legacy_key ) ) {
+ $settings['credentials'][ $current_version ]['site_key'] = $legacy_key;
+ }
+ if ( ! empty( $legacy_secret ) ) {
+ $settings['credentials'][ $current_version ]['site_secret'] = $legacy_secret;
+ }
+
+ // Avoid notoptions cache issue.
+ wp_cache_delete( 'notoptions', 'options' );
+ wp_cache_delete( 'alloptions', 'options' );
+ $updated = \update_option( self::OPTIONS_PREFIX . 'credentials', $settings['credentials'] );
+ if ( $updated ) {
+ \delete_option( self::OPTIONS_PREFIX . 'site_key' );
+ \delete_option( self::OPTIONS_PREFIX . 'site_secret' );
}
}
@@ -272,14 +282,18 @@ public static function get_setting( $key ) {
* Get the reCAPTCHA site key.
*/
public static function get_site_key() {
- return self::get_setting( 'site_key' );
+ $version = self::get_setting( 'version' );
+ $credentials = self::get_setting( 'credentials' );
+ return $credentials[ $version ]['site_key'] ?? '';
}
/**
* Get the reCAPTCHA site secret.
*/
public static function get_site_secret() {
- return self::get_setting( 'site_secret' );
+ $version = self::get_setting( 'version' );
+ $credentials = self::get_setting( 'credentials' );
+ return $credentials[ $version ]['site_secret'] ?? '';
}
/**
@@ -333,6 +347,18 @@ public static function can_use_captcha( $version = null ) {
return true;
}
+ /**
+ * Render reCAPTCHA disclaimer text.
+ */
+ public static function get_terms_text() {
+ return sprintf(
+ /* translators: 1: Privacy Policy URL, 2: Terms of Service URL. */
+ __( 'This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.', 'newspack-plugin' ),
+ 'https://policies.google.com/privacy',
+ 'https://policies.google.com/terms'
+ );
+ }
+
/**
* Verify a REST API request using reCAPTCHA.
* Should work for all versions of reCAPTCHA.
@@ -396,78 +422,12 @@ public static function verify_captcha() {
return true;
}
- /**
- * Render a container for the reCAPTCHA v2 checkbox widget.
- */
- public static function render_recaptcha_v2_container() {
- if ( ! self::can_use_captcha( 'v2' ) ) {
- return;
- }
- ?>
-
-
-
-
- parent() ? get_stylesheet() : get_template() ), [ __CLASS__, 'maybe_update_email_templates' ], 10, 2 );
+ add_action( 'admin_head', [ __CLASS__, 'inject_dynamic_email_template_styles' ] );
}
/**
@@ -63,26 +65,26 @@ public static function register_cpt() {
}
$labels = [
- 'name' => _x( 'Newspack Emails', 'post type general name', 'newspack' ),
- 'singular_name' => _x( 'Newspack Email', 'post type singular name', 'newspack' ),
- 'menu_name' => _x( 'Newspack Emails', 'admin menu', 'newspack' ),
- 'name_admin_bar' => _x( 'Newspack Email', 'add new on admin bar', 'newspack' ),
- 'add_new' => _x( 'Add New', 'popup', 'newspack' ),
- 'add_new_item' => __( 'Add New Newspack Email', 'newspack' ),
- 'new_item' => __( 'New Newspack Email', 'newspack' ),
- 'edit_item' => __( 'Edit Newspack Email', 'newspack' ),
- 'view_item' => __( 'View Newspack Email', 'newspack' ),
- 'all_items' => __( 'All Newspack Emails', 'newspack' ),
- 'search_items' => __( 'Search Newspack Emails', 'newspack' ),
- 'parent_item_colon' => __( 'Parent Newspack Emails:', 'newspack' ),
- 'not_found' => __( 'No Newspack Emails found.', 'newspack' ),
- 'not_found_in_trash' => __( 'No Newspack Emails found in Trash.', 'newspack' ),
- 'items_list' => __( 'Newspack Emails list', 'newspack' ),
- 'item_published' => __( 'Newspack Email published', 'newspack' ),
- 'item_published_privately' => __( 'Newspack Email published privately', 'newspack' ),
- 'item_reverted_to_draft' => __( 'Newspack Email reverted to draft', 'newspack' ),
- 'item_scheduled' => __( 'Newspack Email scheduled', 'newspack' ),
- 'item_updated' => __( 'Newspack Email updated', 'newspack' ),
+ 'name' => _x( 'Newspack Emails', 'post type general name', 'newspack-plugin' ),
+ 'singular_name' => _x( 'Newspack Email', 'post type singular name', 'newspack-plugin' ),
+ 'menu_name' => _x( 'Newspack Emails', 'admin menu', 'newspack-plugin' ),
+ 'name_admin_bar' => _x( 'Newspack Email', 'add new on admin bar', 'newspack-plugin' ),
+ 'add_new' => _x( 'Add New', 'popup', 'newspack-plugin' ),
+ 'add_new_item' => __( 'Add New Newspack Email', 'newspack-plugin' ),
+ 'new_item' => __( 'New Newspack Email', 'newspack-plugin' ),
+ 'edit_item' => __( 'Edit Newspack Email', 'newspack-plugin' ),
+ 'view_item' => __( 'View Newspack Email', 'newspack-plugin' ),
+ 'all_items' => __( 'All Newspack Emails', 'newspack-plugin' ),
+ 'search_items' => __( 'Search Newspack Emails', 'newspack-plugin' ),
+ 'parent_item_colon' => __( 'Parent Newspack Emails:', 'newspack-plugin' ),
+ 'not_found' => __( 'No Newspack Emails found.', 'newspack-plugin' ),
+ 'not_found_in_trash' => __( 'No Newspack Emails found in Trash.', 'newspack-plugin' ),
+ 'items_list' => __( 'Newspack Emails list', 'newspack-plugin' ),
+ 'item_published' => __( 'Newspack Email published', 'newspack-plugin' ),
+ 'item_published_privately' => __( 'Newspack Email published privately', 'newspack-plugin' ),
+ 'item_reverted_to_draft' => __( 'Newspack Email reverted to draft', 'newspack-plugin' ),
+ 'item_scheduled' => __( 'Newspack Email scheduled', 'newspack-plugin' ),
+ 'item_updated' => __( 'Newspack Email updated', 'newspack-plugin' ),
];
\register_post_type(
@@ -170,20 +172,69 @@ public static function get_email_payload( $config_name, $placeholders = [] ) {
$email_config = self::get_email_config_by_type( $config_name );
$html = $email_config['html_payload'];
$reply_to_email = $email_config['reply_to_email'];
- $placeholders = array_merge(
+ $site_address = '';
+
+ if ( class_exists( 'WC' ) ) {
+ $base_address = WC()->countries->get_base_address();
+ $base_city = WC()->countries->get_base_city();
+ $base_postcode = WC()->countries->get_base_postcode();
+ } else {
+ $base_address = get_option( 'woocommerce_store_address', '' );
+ $base_city = get_option( 'woocommerce_store_city', '' );
+ $base_postcode = get_option( 'woocommerce_store_postcode', '' );
+ }
+
+ if ( $base_address ) {
+ if ( ! $base_city && ! $base_postcode ) {
+ $site_address = $base_address;
+ } else {
+ $site_address = sprintf(
+ // translators: formatted store address where 1 is street address, 2 is city, and 3 is postcode.
+ __( '%1$s, %2$s %3$s', 'newspack-plugin' ),
+ $base_address,
+ $base_city,
+ $base_postcode
+ );
+ }
+ }
+
+ if ( $site_address ) {
+ $site_contact = sprintf(
+ /* Translators: 1: site title 2: site base address. */
+ __( '%1$s — %2$s', 'newspack-plugin' ),
+ '' . get_bloginfo( 'name' ) . '',
+ $site_address
+ );
+ } else {
+ $site_contact = get_bloginfo( 'name' );
+ }
+
+ $placeholders = array_merge(
[
[
'template' => '*CONTACT_EMAIL*',
'value' => sprintf( '%s', $reply_to_email, $reply_to_email ),
],
[
- 'template' => '*SITE_URL*',
- 'value' => get_site_url(),
+ 'template' => '*SITE_ADDRESS*',
+ 'value' => $site_address,
+ ],
+ [
+ 'template' => '*SITE_CONTACT*',
+ 'value' => $site_contact,
],
[
'template' => '*SITE_LOGO*',
'value' => esc_url( wp_get_attachment_url( get_theme_mod( 'custom_logo' ) ) ),
],
+ [
+ 'template' => '*SITE_TITLE*',
+ 'value' => get_bloginfo( 'name' ),
+ ],
+ [
+ 'template' => '*SITE_URL*',
+ 'value' => get_bloginfo( 'wpurl' ),
+ ],
],
$placeholders
);
@@ -209,6 +260,35 @@ public static function send_email( $config_name, $to, $placeholders = [] ) {
return false;
}
+ // Migrate to RAS-ACC email templates if migration option is not set AND there have been no manual updates to the templates.
+ if ( get_option( 'newspack_email_templates_migrated', '' ) !== 'v1' ) {
+ $migrated = true;
+ $templates = get_posts(
+ [
+ 'post_type' => self::POST_TYPE,
+ 'posts_per_page' => -1,
+ 'post_status' => 'publish',
+ ]
+ );
+
+ foreach ( $templates as $template ) {
+ $publish_date = get_the_date( 'Y-m-d H:i:s', $template->ID );
+ $last_modified_date = get_the_modified_date( 'Y-m-d H:i:s', $template->ID );
+
+ // Template has not been modified, so trash the post so we can trigger a template update.
+ if ( $publish_date === $last_modified_date ) {
+ if ( ! wp_trash_post( $template->ID ) ) {
+ // Flag the migration as failed so we can trigger another attempt later.
+ $migrated = false;
+ }
+ }
+ }
+
+ if ( $migrated ) {
+ update_option( 'newspack_email_templates_migrated', 'v1' );
+ }
+ }
+
$switched_locale = \switch_to_locale( \get_user_locale( \wp_get_current_user() ) );
if ( 'string' === gettype( $config_name ) ) {
@@ -335,6 +415,7 @@ private static function serialize_email( $type = null, $post_id = 0 ) {
$edit_link = str_replace( site_url(), '', $post_link );
}
$serialized_email = [
+ 'type' => $type,
'label' => $email_config['label'],
'description' => $email_config['description'],
'post_id' => $post_id,
@@ -428,6 +509,21 @@ public static function get_email_config_by_type( $type ) {
/** Only attempt to create the email post if wp-includes/pluggable.php is loaded. */
return false;
} else {
+ // Make sure newsletters color palette is updated with latest theme colors.
+ if ( self::supports_emails() && method_exists( '\Newspack_Newsletters', 'update_color_palette' ) ) {
+ $theme_colors = newspack_get_theme_colors();
+ \Newspack_Newsletters::update_color_palette(
+ [
+ 'primary' => $theme_colors['primary_color'],
+ 'primary-text' => $theme_colors['primary_text_color'],
+ 'primary-variation' => $theme_colors['primary_variation'],
+ 'secondary' => $theme_colors['secondary_color'],
+ 'secondary-text' => $theme_colors['secondary_text_color'],
+ 'secondary-variation' => $theme_colors['secondary_variation'],
+ ]
+ );
+ }
+
$email_post_data = self::load_email_template( $type );
if ( ! $email_post_data ) {
Logger::error( 'Error: could not retrieve template for type: ' . $type );
@@ -438,6 +534,8 @@ public static function get_email_config_by_type( $type ) {
$email_post_data['meta_input'] = [
self::EMAIL_CONFIG_NAME_META => $type,
\Newspack_Newsletters::EMAIL_HTML_META => $email_post_data['email_html'],
+ 'font_body' => 'Arial, Helvetica, sans-serif',
+ 'font_header' => 'Arial, Helvetica, sans-serif',
];
$post_id = wp_insert_post( $email_post_data );
Logger::log( sprintf( 'Creating email of type %s (id: %s).', $type, $post_id ) );
@@ -490,7 +588,7 @@ public static function api_send_test_email( $request ) {
} else {
return new \WP_Error(
'newspack_test_email_not_sent',
- __( 'Test email was not sent.', 'newspack' )
+ __( 'Test email was not sent.', 'newspack-plugin' )
);
}
}
@@ -531,7 +629,7 @@ public static function api_permissions_check( $request ) {
if ( ! current_user_can( 'manage_options' ) ) {
return new \WP_Error(
'newspack_rest_forbidden',
- esc_html__( 'You cannot use this resource.', 'newspack' ),
+ esc_html__( 'You cannot use this resource.', 'newspack-plugin' ),
[
'status' => 403,
]
@@ -559,5 +657,112 @@ public static function get_password_reset_url( $user, $key ) {
return wp_lostpassword_url();
}
+
+ /**
+ * Trigger an update to all email template posts when theme color is updated in customizer.
+ * This is to force an update of dynamic properties such as theme colors.
+ *
+ * @param string|array $previous_value previous option value.
+ * @param string|array $updated_value updated option value.
+ *
+ * @return void
+ */
+ public static function maybe_update_email_templates( $previous_value, $updated_value ) {
+ // Do nothing if newsletters is not active.
+ if ( ! self::supports_emails() ) {
+ return;
+ }
+
+ // Check for theme mod color settings in case a non-newspack theme is installed.
+ if ( ! isset( $previous_value['primary_color_hex'], $updated_value['primary_color_hex'] ) ) {
+ return;
+ }
+
+ if ( ( $previous_value['primary_color_hex'] !== $updated_value['primary_color_hex'] ) || ( $previous_value['secondary_color_hex'] !== $updated_value['secondary_color_hex'] ) ) {
+ // Update the newsletters color palette.
+ $updated = \Newspack_Newsletters::update_color_palette(
+ [
+ 'primary' => $updated_value['primary_color_hex'],
+ 'primary-text' => newspack_get_color_contrast( $updated_value['primary_color_hex'] ),
+ 'primary-variation' => newspack_adjust_brightness( $updated_value['primary_color_hex'], -40 ),
+ 'secondary' => $updated_value['secondary_color_hex'],
+ 'secondary-text' => newspack_get_color_contrast( $updated_value['secondary_color_hex'] ),
+ 'secondary-variation' => newspack_adjust_brightness( $updated_value['secondary_color_hex'], -40 ),
+
+ ]
+ );
+
+ if ( ! $updated ) {
+ Logger::error( 'There was an error updating the newsletters color palette.' );
+ }
+
+ // Trigger an update of all email templates to regenerate HTML.
+ $templates = get_posts(
+ [
+ 'post_type' => self::POST_TYPE,
+ 'posts_per_page' => -1,
+ 'post_status' => 'publish',
+ ]
+ );
+
+ foreach ( $templates as $template ) {
+ // Find/replace the old hex values with the new ones in the rendered email HTML.
+ $email_html = get_post_meta( $template->ID, \Newspack_Newsletters::EMAIL_HTML_META, true );
+ $email_html = str_replace(
+ [
+ $previous_value['primary_color_hex'],
+ newspack_get_color_contrast( $previous_value['primary_color_hex'] ),
+ newspack_adjust_brightness( $previous_value['primary_color_hex'], -40 ),
+ $previous_value['secondary_color_hex'],
+ newspack_get_color_contrast( $previous_value['secondary_color_hex'] ),
+ newspack_adjust_brightness( $previous_value['secondary_color_hex'], -40 ),
+ ],
+ [
+ $updated_value['primary_color_hex'],
+ newspack_get_color_contrast( $updated_value['primary_color_hex'] ),
+ newspack_adjust_brightness( $updated_value['primary_color_hex'], -40 ),
+ $updated_value['secondary_color_hex'],
+ newspack_get_color_contrast( $updated_value['secondary_color_hex'] ),
+ newspack_adjust_brightness( $updated_value['secondary_color_hex'], -40 ),
+ ],
+ $email_html
+ );
+ update_post_meta( $template->ID, \Newspack_Newsletters::EMAIL_HTML_META, $email_html );
+
+ wp_update_post( [ 'ID' => $template->ID ] );
+ }
+ }
+ }
+
+ /**
+ * Inject dynamic email template styles for dynamic text colors in the editor.
+ *
+ * @return void
+ */
+ public static function inject_dynamic_email_template_styles() {
+ if ( get_post_type() !== self::POST_TYPE ) {
+ return;
+ }
+
+ [ 'primary_text_color' => $primary_text_color ] = newspack_get_theme_colors();
+
+ ?>
+
+ __( 'the site base address', 'newspack-plugin' ),
+ 'template' => '*SITE_ADDRESS*',
+ ],
+ [
+ 'label' => __( 'the site contact info, including site name and address', 'newspack-plugin' ),
+ 'template' => '*SITE_CONTACT*',
+ ],
+ [
+ 'label' => __( 'the site title', 'newspack-plugin' ),
+ 'template' => '*SITE_TITLE*',
+ ],
+ [
+ 'label' => __( 'the site url', 'newspack-plugin' ),
+ 'template' => '*SITE_URL*',
+ ],
+ ];
$configs[ self::EMAIL_TYPES['VERIFICATION'] ] = [
'name' => self::EMAIL_TYPES['VERIFICATION'],
- 'label' => __( 'Verification', 'newspack' ),
- 'description' => __( "Email sent to the reader after they've registered.", 'newspack' ),
+ 'label' => __( 'Verification', 'newspack-plugin' ),
+ 'description' => __( "Email sent to the reader after they've registered.", 'newspack-plugin' ),
'template' => dirname( NEWSPACK_PLUGIN_FILE ) . '/includes/templates/reader-activation-emails/verification.php',
- 'editor_notice' => __( 'This email will be sent to a reader after they\'ve registered.', 'newspack' ),
- 'available_placeholders' => [
+ 'editor_notice' => __( 'This email will be sent to a reader after they\'ve registered.', 'newspack-plugin' ),
+ 'available_placeholders' => array_merge(
+ $available_placeholders,
[
- 'label' => __( 'the verification link', 'newspack' ),
- 'template' => '*VERIFICATION_URL*',
- ],
- ],
+ [
+ 'label' => __( 'the verification link', 'newspack-plugin' ),
+ 'template' => '*VERIFICATION_URL*',
+ ],
+ ]
+ ),
];
$configs[ self::EMAIL_TYPES['MAGIC_LINK'] ] = [
'name' => self::EMAIL_TYPES['MAGIC_LINK'],
- 'label' => __( 'Login link', 'newspack' ),
- 'description' => __( 'Email with a login link.', 'newspack' ),
+ 'label' => __( 'Login link', 'newspack-plugin' ),
+ 'description' => __( 'Email sent to users with a login link generated by Admin user or when one-time password is disabled.', 'newspack-plugin' ),
'template' => dirname( NEWSPACK_PLUGIN_FILE ) . '/includes/templates/reader-activation-emails/magic-link.php',
- 'editor_notice' => __( 'This email will be sent to a reader when they request a login link.', 'newspack' ),
- 'available_placeholders' => [
+ 'editor_notice' => __( 'This email will be sent to a reader when they request a login link.', 'newspack-plugin' ),
+ 'available_placeholders' => array_merge(
+ $available_placeholders,
[
- 'label' => __( 'the one-time password', 'newspack' ),
- 'template' => '*MAGIC_LINK_OTP*',
- ],
- ],
+ [
+ 'label' => __( 'the one-time password', 'newspack-plugin' ),
+ 'template' => '*MAGIC_LINK_OTP*',
+ ],
+ ]
+ ),
];
$configs[ self::EMAIL_TYPES['OTP_AUTH'] ] = [
'name' => self::EMAIL_TYPES['OTP_AUTH'],
- 'label' => __( 'Login one-time password', 'newspack' ),
- 'description' => __( 'Email with a one-time password and login link.', 'newspack' ),
+ 'label' => __( 'Login one-time password', 'newspack-plugin' ),
+ 'description' => __( 'Email sent to users with a one-time password and login link.', 'newspack-plugin' ),
'template' => dirname( NEWSPACK_PLUGIN_FILE ) . '/includes/templates/reader-activation-emails/otp.php',
- 'editor_notice' => __( 'This email will be sent to a reader when they request a login link and a one-time password is available.', 'newspack' ),
- 'available_placeholders' => [
- [
- 'label' => __( 'the one-time password', 'newspack' ),
- 'template' => '*MAGIC_LINK_OTP*',
- ],
+ 'editor_notice' => __( 'This email will be sent to a reader when they request a login link and a one-time password is available.', 'newspack-plugin' ),
+ 'available_placeholders' => array_merge(
+ $available_placeholders,
[
- 'label' => __( 'the login link', 'newspack' ),
- 'template' => '*MAGIC_LINK_URL*',
- ],
- ],
+ [
+ 'label' => __( 'the one-time password', 'newspack-plugin' ),
+ 'template' => '*MAGIC_LINK_OTP*',
+ ],
+ [
+ 'label' => __( 'the login link', 'newspack-plugin' ),
+ 'template' => '*MAGIC_LINK_URL*',
+ ],
+ [
+ 'label' => __( 'the password reset link', 'newspack-plugin' ),
+ 'template' => '*SET_PASSWORD_LINK*',
+ ],
+ ]
+ ),
];
$configs[ self::EMAIL_TYPES['RESET_PASSWORD'] ] = [
'name' => self::EMAIL_TYPES['RESET_PASSWORD'],
- 'label' => __( 'Set a New Password', 'newspack' ),
- 'description' => __( 'Email with password reset link.', 'newspack' ),
+ 'label' => __( 'Set a New Password', 'newspack-plugin' ),
+ 'description' => __( 'Email with password reset link.', 'newspack-plugin' ),
'template' => dirname( NEWSPACK_PLUGIN_FILE ) . '/includes/templates/reader-activation-emails/password-reset.php',
- 'editor_notice' => __( 'This email will be sent to a reader when they request a password creation or reset.', 'newspack' ),
- 'available_placeholders' => [
+ 'editor_notice' => __( 'This email will be sent to a reader when they request a password creation or reset.', 'newspack-plugin' ),
+ 'available_placeholders' => array_merge(
+ $available_placeholders,
[
- 'label' => __( 'the password reset link', 'newspack' ),
- 'template' => '*PASSWORD_RESET_LINK*',
- ],
- ],
+ [
+ 'label' => __( 'the password reset link', 'newspack-plugin' ),
+ 'template' => '*PASSWORD_RESET_LINK*',
+ ],
+ ]
+ ),
];
$configs[ self::EMAIL_TYPES['DELETE_ACCOUNT'] ] = [
'name' => self::EMAIL_TYPES['DELETE_ACCOUNT'],
- 'label' => __( 'Delete Account', 'newspack' ),
- 'description' => __( 'Email with account deletion link.', 'newspack' ),
+ 'label' => __( 'Delete Account', 'newspack-plugin' ),
+ 'description' => __( 'Email with account deletion link.', 'newspack-plugin' ),
'template' => dirname( NEWSPACK_PLUGIN_FILE ) . '/includes/templates/reader-activation-emails/delete-account.php',
- 'editor_notice' => __( 'This email will be sent to a reader when they request an account deletion.', 'newspack' ),
+ 'editor_notice' => __( 'This email will be sent to a reader when they request an account deletion.', 'newspack-plugin' ),
'available_placeholders' => [
[
- 'label' => __( 'the account deletion link', 'newspack' ),
+ 'label' => __( 'the account deletion link', 'newspack-plugin' ),
'template' => '*DELETION_LINK*',
],
],
diff --git a/includes/reader-activation/class-reader-activation.php b/includes/reader-activation/class-reader-activation.php
index 67c79a57a0..bbe44d5802 100644
--- a/includes/reader-activation/class-reader-activation.php
+++ b/includes/reader-activation/class-reader-activation.php
@@ -19,10 +19,11 @@ final class Reader_Activation {
const OPTIONS_PREFIX = 'newspack_reader_activation_';
- const AUTH_READER_COOKIE = 'np_auth_reader';
- const AUTH_INTENTION_COOKIE = 'np_auth_intention';
- const SCRIPT_HANDLE = 'newspack-reader-activation';
- const AUTH_SCRIPT_HANDLE = 'newspack-reader-auth';
+ const AUTH_READER_COOKIE = 'np_auth_reader';
+ const AUTH_INTENTION_COOKIE = 'np_auth_intention';
+ const SCRIPT_HANDLE = 'newspack-reader-activation';
+ const AUTH_SCRIPT_HANDLE = 'newspack-reader-auth';
+ const NEWSLETTERS_SCRIPT_HANDLE = 'newspack-newsletters-signup';
/**
* Reader user meta keys.
@@ -45,9 +46,10 @@ final class Reader_Activation {
*/
const AUTH_FORM_ACTION = 'reader-activation-auth-form';
const AUTH_FORM_OPTIONS = [
- 'pwd',
- 'link',
+ 'signin',
'register',
+ 'link',
+ 'pwd',
];
/**
@@ -55,12 +57,34 @@ final class Reader_Activation {
*/
const SSO_REGISTRATION_METHODS = [ 'google' ];
+ /**
+ * Newsletters signup form.
+ */
+ const NEWSLETTERS_SIGNUP_FORM_ACTION = 'reader-activation-newsletters-signup';
+
+ /**
+ * Whether the session is authenticating a newly registered reader
+ *
+ * @var bool
+ */
+ private static $is_new_reader_auth = false;
+
+ /**
+ * UI labels for reader activation flows.
+ *
+ * @var mixed[]
+ */
+ private static $reader_activation_labels = [];
+
/**
* Initialize hooks.
*/
public static function init() {
\add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_scripts' ] );
- \add_action( 'wp_footer', [ __CLASS__, 'render_auth_form' ] );
+ \add_action( 'wp_footer', [ __CLASS__, 'render_auth_modal' ] );
+ \add_action( 'wp_footer', [ __CLASS__, 'render_newsletters_signup_modal' ] );
+ \add_action( 'wp_ajax_newspack_reader_activation_newsletters_signup', [ __CLASS__, 'newsletters_signup' ] );
+ \add_action( 'woocommerce_customer_reset_password', [ __CLASS__, 'login_after_password_reset' ] );
if ( self::is_enabled() ) {
\add_action( 'clear_auth_cookie', [ __CLASS__, 'clear_auth_intention_cookie' ] );
@@ -71,6 +95,7 @@ public static function init() {
\add_action( 'resetpass_form', [ __CLASS__, 'set_reader_verified' ] );
\add_action( 'password_reset', [ __CLASS__, 'set_reader_verified' ] );
\add_action( 'password_reset', [ __CLASS__, 'set_reader_has_password' ] );
+ \add_action( 'profile_update', [ __CLASS__, 'maybe_set_reader_has_password' ], 10, 3 );
\add_action( 'newspack_magic_link_authenticated', [ __CLASS__, 'set_reader_verified' ] );
\add_action( 'auth_cookie_expiration', [ __CLASS__, 'auth_cookie_expiration' ], 10, 3 );
\add_action( 'init', [ __CLASS__, 'setup_nav_menu' ] );
@@ -97,77 +122,197 @@ public static function enqueue_scripts() {
*
* @param bool $allow_reg_block_render Whether to allow the registration block to render.
*/
- if ( ! apply_filters( 'newspack_reader_activation_should_render_auth', true ) ) {
- return;
+ if ( apply_filters( 'newspack_reader_activation_should_render_auth', true ) ) {
+ $authenticated_email = self::get_logged_in_reader_email_address();
+ $script_dependencies = [];
+ $script_data = [
+ 'auth_intention_cookie' => self::AUTH_INTENTION_COOKIE,
+ 'cid_cookie' => NEWSPACK_CLIENT_ID_COOKIE_NAME,
+ 'is_logged_in' => \is_user_logged_in(),
+ 'authenticated_email' => $authenticated_email,
+ 'otp_auth_action' => Magic_Link::OTP_AUTH_ACTION,
+ 'otp_rate_interval' => Magic_Link::RATE_INTERVAL,
+ 'account_url' => function_exists( 'wc_get_account_endpoint_url' ) ? \wc_get_account_endpoint_url( 'dashboard' ) : '',
+ 'is_ras_enabled' => self::is_enabled(),
+ ];
+
+ if ( Recaptcha::can_use_captcha() ) {
+ $recaptcha_version = Recaptcha::get_setting( 'version' );
+ $script_dependencies[] = Recaptcha::SCRIPT_HANDLE;
+ if ( 'v3' === $recaptcha_version ) {
+ $script_data['captcha_site_key'] = Recaptcha::get_site_key();
+ }
+ }
+
+ Newspack::load_common_assets();
+
+ /**
+ * Reader Activation Frontend Library.
+ */
+ \wp_enqueue_script(
+ self::SCRIPT_HANDLE,
+ Newspack::plugin_url() . '/dist/reader-activation.js',
+ $script_dependencies,
+ NEWSPACK_PLUGIN_VERSION,
+ [
+ 'strategy' => 'async',
+ 'in_footer' => true,
+ ]
+ );
+ \wp_localize_script(
+ self::SCRIPT_HANDLE,
+ 'newspack_ras_config',
+ $script_data
+ );
+ \wp_script_add_data( self::SCRIPT_HANDLE, 'async', true );
+ \wp_script_add_data( self::SCRIPT_HANDLE, 'amp-plus', true );
+
+ /**
+ * Reader Authentication
+ */
+ \wp_enqueue_script(
+ self::AUTH_SCRIPT_HANDLE,
+ Newspack::plugin_url() . '/dist/reader-auth.js',
+ [ self::SCRIPT_HANDLE ],
+ NEWSPACK_PLUGIN_VERSION,
+ [
+ 'strategy' => 'async',
+ 'in_footer' => true,
+ ]
+ );
+ \wp_localize_script( self::AUTH_SCRIPT_HANDLE, 'newspack_reader_activation_labels', self::get_reader_activation_labels() );
+ \wp_script_add_data( self::AUTH_SCRIPT_HANDLE, 'async', true );
+ \wp_script_add_data( self::AUTH_SCRIPT_HANDLE, 'amp-plus', true );
+ \wp_enqueue_style(
+ self::AUTH_SCRIPT_HANDLE,
+ Newspack::plugin_url() . '/dist/reader-auth.css',
+ [],
+ NEWSPACK_PLUGIN_VERSION
+ );
}
- $authenticated_email = '';
- if ( \is_user_logged_in() && self::is_user_reader( \wp_get_current_user() ) ) {
- $authenticated_email = \wp_get_current_user()->user_email;
- }
- $script_dependencies = [];
- $script_data = [
- 'auth_intention_cookie' => self::AUTH_INTENTION_COOKIE,
- 'cid_cookie' => NEWSPACK_CLIENT_ID_COOKIE_NAME,
- 'authenticated_email' => $authenticated_email,
- 'otp_auth_action' => Magic_Link::OTP_AUTH_ACTION,
- 'account_url' => function_exists( 'wc_get_account_endpoint_url' ) ? \wc_get_account_endpoint_url( 'dashboard' ) : '',
- ];
+ if ( self::is_newsletters_signup_available() ) {
+ /**
+ * Newsletters Signup.
+ */
+ \wp_enqueue_script(
+ self::NEWSLETTERS_SCRIPT_HANDLE,
+ Newspack::plugin_url() . '/dist/newsletters-signup.js',
+ [ self::SCRIPT_HANDLE ],
+ NEWSPACK_PLUGIN_VERSION,
+ [
+ 'strategy' => 'async',
+ 'in_footer' => true,
+ ]
+ );
- if ( Recaptcha::can_use_captcha() ) {
- $recaptcha_version = Recaptcha::get_setting( 'version' );
- $script_dependencies[] = Recaptcha::SCRIPT_HANDLE;
- if ( 'v3' === $recaptcha_version ) {
- $script_data['captcha_site_key'] = Recaptcha::get_site_key();
+ \wp_localize_script(
+ self::NEWSLETTERS_SCRIPT_HANDLE,
+ 'newspack_reader_activation_newsletters',
+ [
+ 'newspack_ajax_url' => admin_url( 'admin-ajax.php' ),
+ ]
+ );
+
+ \wp_script_add_data( self::NEWSLETTERS_SCRIPT_HANDLE, 'async', true );
+ \wp_enqueue_style(
+ self::NEWSLETTERS_SCRIPT_HANDLE,
+ Newspack::plugin_url() . '/dist/newsletters-signup.css',
+ [],
+ NEWSPACK_PLUGIN_VERSION
+ );
+ }
+ }
+
+ /**
+ * Get labels for reader activation flows.
+ *
+ * @param string|null $key Key of the label to return (optional).
+ *
+ * @return mixed[]|string The label string or an array of labels keyed by string.
+ */
+ private static function get_reader_activation_labels( $key = null ) {
+ if ( empty( self::$reader_activation_labels ) ) {
+ $default_labels = [
+ 'title' => __( 'Sign in', 'newspack-plugin' ),
+ 'invalid_email' => __( 'Please enter a valid email address.', 'newspack-plugin' ),
+ 'invalid_password' => __( 'Please enter a password.', 'newspack-plugin' ),
+ 'invalid_display' => __( 'Display name cannot match your email address. Please choose a different display name.', 'newspack-plugin' ),
+ 'blocked_popup' => __( 'The popup has been blocked. Allow popups for the site and try again.', 'newspack-plugin' ),
+ 'code_sent' => __( 'Code sent! Check your inbox.', 'newspack-plugin' ),
+ 'code_resent' => __( 'Code resent! Check your inbox.', 'newspack-plugin' ),
+ 'create_account' => __( 'Create an account', 'newspack-plugin' ),
+ 'signin' => [
+ 'title' => __( 'Sign in', 'newspack-plugin' ),
+ 'success_title' => __( 'Success! You’re signed in.', 'newspack-plugin' ),
+ 'success_message' => __( 'Login successful!', 'newspack-plugin' ),
+ 'continue' => __( 'Continue', 'newspack-plugin' ),
+ 'resend_code' => __( 'Resend code', 'newspack-plugin' ),
+ 'otp' => __( 'Email me a one-time code instead', 'newspack-plugin' ),
+ 'otp_title' => __( 'Enter the code sent to your email.', 'newspack-plugin' ),
+ 'forgot_password' => __( 'Forgot password', 'newspack-plugin' ),
+ 'create_account' => __( 'Create an account', 'newspack-plugin' ),
+ 'register' => __( 'Sign in to an existing account', 'newspack-plugin' ),
+ 'go_back' => __( 'Go back', 'newspack-plugin' ),
+ 'set_password' => __( 'Set a password (optional)', 'newspack-plugin' ),
+ ],
+ 'register' => [
+ 'title' => __( 'Create an account', 'newspack-plugin' ),
+ 'success_title' => __( 'Success! Your account was created and you’re signed in.', 'newspack-plugin' ),
+ 'success_description' => __( 'In the future, you’ll sign in with a magic link, or a code sent to your email. If you’d rather use a password, you can set one below.', 'newspack-plugin' ),
+ ],
+ 'verify' => __( 'Thank you for verifying your account!', 'newspack-plugin' ),
+ 'magic_link' => __( 'Please check your inbox for an authentication link.', 'newspack-plugin' ),
+ 'password_reset_interval' => __( 'Please wait a moment before requesting another password reset email.', 'newspack-plugin' ),
+ 'account_link' => [
+ 'signedin' => __( 'My Account', 'newspack-plugin' ),
+ 'signedout' => __( 'Sign In', 'newspack-plugin' ),
+ ],
+ 'newsletters_cta' => __( 'Subscribe to our newsletter', 'newspack-plugin' ),
+ 'newsletters_confirmation' => sprintf(
+ // Translators: %s is the site name.
+ __( 'Thanks for supporting %s.', 'newspack-plugin' ),
+ get_option( 'blogname' )
+ ),
+ 'newsletters_continue' => __( 'Continue', 'newspack-plugin' ),
+ 'newsletters_details' => sprintf(
+ // Translators: %s is the site name.
+ __( 'Get the best of %s directly in your email inbox.', 'newspack-plugin' ),
+ get_bloginfo( 'name' )
+ ),
+ 'newsletters_success' => __( 'Signup successful!', 'newspack-plugin' ),
+ 'newsletters_title' => __( 'Sign up for newsletters', 'newspack-plugin' ),
+ 'auth_form_action' => self::AUTH_FORM_ACTION,
+ ];
+
+ /**
+ * Filters the global labels for reader activation auth flow.
+ *
+ * @param mixed[] $labels Labels keyed by name.
+ */
+ $filtered_labels = apply_filters( 'newspack_reader_activation_auth_labels', $default_labels );
+
+ foreach ( $default_labels as $key => $label ) {
+ if ( isset( $filtered_labels[ $key ] ) ) {
+ if ( is_array( $label ) && is_array( $filtered_labels[ $key ] ) ) {
+ self::$reader_activation_labels[ $key ] = array_merge( $label, $filtered_labels[ $key ] );
+ } elseif ( is_string( $label ) && is_string( $filtered_labels[ $key ] ) ) {
+ self::$reader_activation_labels[ $key ] = $filtered_labels[ $key ];
+ } else {
+ // If filtered label type doesn't match, fallback to default.
+ self::$reader_activation_labels[ $key ] = $label;
+ }
+ } else {
+ self::$reader_activation_labels[ $key ] = $label;
+ }
}
}
- /**
- * Reader Activation Frontend Library.
- */
- \wp_enqueue_script(
- self::SCRIPT_HANDLE,
- Newspack::plugin_url() . '/dist/reader-activation.js',
- $script_dependencies,
- NEWSPACK_PLUGIN_VERSION,
- true
- );
- \wp_localize_script(
- self::SCRIPT_HANDLE,
- 'newspack_ras_config',
- $script_data
- );
- \wp_script_add_data( self::SCRIPT_HANDLE, 'async', true );
- \wp_script_add_data( self::SCRIPT_HANDLE, 'amp-plus', true );
+ if ( ! $key ) {
+ return self::$reader_activation_labels;
+ }
- /**
- * Reader Authentication
- */
- \wp_enqueue_script(
- self::AUTH_SCRIPT_HANDLE,
- Newspack::plugin_url() . '/dist/reader-auth.js',
- [ self::SCRIPT_HANDLE ],
- NEWSPACK_PLUGIN_VERSION,
- true
- );
- \wp_localize_script(
- self::AUTH_SCRIPT_HANDLE,
- 'newspack_reader_auth_labels',
- [
- 'invalid_email' => __( 'Please enter a valid email address.', 'newspack-plugin' ),
- 'invalid_password' => __( 'Please enter a password.', 'newspack-plugin' ),
- 'blocked_popup' => __( 'The popup has been blocked. Allow popups for the site and try again.', 'newspack-plugin' ),
- 'login_canceled' => __( 'Login canceled.', 'newspack-plugin' ),
- ]
- );
- \wp_script_add_data( self::AUTH_SCRIPT_HANDLE, 'async', true );
- \wp_script_add_data( self::AUTH_SCRIPT_HANDLE, 'amp-plus', true );
- \wp_enqueue_style(
- self::AUTH_SCRIPT_HANDLE,
- Newspack::plugin_url() . '/dist/reader-auth.css',
- [],
- NEWSPACK_PLUGIN_VERSION
- );
+ return self::$reader_activation_labels[ $key ] ?? '';
}
/**
@@ -177,25 +322,29 @@ public static function enqueue_scripts() {
*/
private static function get_settings_config() {
$settings_config = [
- 'enabled' => false,
- 'enabled_account_link' => true,
- 'account_link_menu_locations' => [ 'tertiary-menu' ],
- 'newsletters_label' => __( 'Subscribe to our newsletters:', 'newspack-plugin' ),
- 'use_custom_lists' => false,
- 'newsletter_lists' => [],
- 'terms_text' => '',
- 'terms_url' => '',
- 'sync_esp' => true,
- 'metadata_prefix' => Sync\Metadata::get_prefix(),
- 'metadata_fields' => Sync\Metadata::get_fields(),
- 'sync_esp_delete' => true,
- 'active_campaign_master_list' => '',
- 'mailchimp_audience_id' => '',
- 'mailchimp_reader_default_status' => 'transactional',
- 'emails' => Emails::get_emails( array_values( Reader_Activation_Emails::EMAIL_TYPES ), false ),
- 'sender_name' => Emails::get_from_name(),
- 'sender_email_address' => Emails::get_from_email(),
- 'contact_email_address' => Emails::get_reply_to_email(),
+ 'enabled' => false,
+ 'enabled_account_link' => true,
+ 'account_link_menu_locations' => [ 'tertiary-menu' ],
+ 'newsletters_label' => self::get_reader_activation_labels( 'newsletters_cta' ),
+ 'use_custom_lists' => false,
+ 'newsletter_lists' => [],
+ 'terms_text' => '',
+ 'terms_url' => '',
+ 'sync_esp' => true,
+ 'metadata_prefix' => Sync\Metadata::get_prefix(),
+ 'metadata_fields' => Sync\Metadata::get_fields(),
+ 'sync_esp_delete' => true,
+ 'active_campaign_master_list' => '',
+ 'mailchimp_audience_id' => '',
+ 'mailchimp_reader_default_status' => 'transactional',
+ 'emails' => Emails::get_emails( array_values( Reader_Activation_Emails::EMAIL_TYPES ), false ),
+ 'sender_name' => Emails::get_from_name(),
+ 'sender_email_address' => Emails::get_from_email(),
+ 'contact_email_address' => Emails::get_reply_to_email(),
+ 'woocommerce_registration_required' => false,
+ 'woocommerce_checkout_privacy_policy_text' => self::get_checkout_privacy_policy_text(),
+ 'woocommerce_post_checkout_success_text' => self::get_post_checkout_success_text(),
+ 'woocommerce_post_checkout_registration_success_text' => self::get_post_checkout_registration_success_text(),
];
/**
@@ -381,11 +530,66 @@ public static function set_mailchimp_sync_contact_status( $contact ) {
* @return array
*/
public static function get_registration_newsletter_lists() {
+ if ( ! class_exists( '\Newspack_Newsletters_Subscription' ) ) {
+ return [];
+ }
+
+ $registration_lists = self::get_available_newsletter_lists();
+
+ /**
+ * Filters the newsletters lists that should be rendered during registration.
+ *
+ * @param array $registration_lists Array of newsletter lists.
+ */
+ return apply_filters( 'newspack_registration_newsletters_lists', $registration_lists );
+ }
+
+ /**
+ * Get the newsletter lists that should be rendered after checkout.
+ *
+ * @param string $email_address Email address. Optional.
+ *
+ * @return array
+ */
+ public static function get_post_checkout_newsletter_lists( $email_address = '' ) {
+ $available_lists = self::get_available_newsletter_lists( $email_address );
+ $registration_lists = [];
+
+ if ( empty( $available_lists ) ) {
+ return [];
+ }
+
+ foreach ( $available_lists as $list_id => $list ) {
+ // Skip any premium lists since the reader has already made a purchase at this stage.
+ if ( method_exists( '\Newspack_Newsletters\Plugins\Woocommerce_Memberships', 'is_subscription_list_tied_to_plan' ) && \Newspack_Newsletters\Plugins\Woocommerce_Memberships::is_subscription_list_tied_to_plan( $list['db_id'] ) ) {
+ continue;
+ }
+
+ $registration_lists[ $list_id ] = $list;
+ }
+
+ /**
+ * Filters the newsletters lists that should be rendered after checkout.
+ *
+ * @param array $registration_lists Array of newsletter lists.
+ */
+ return apply_filters( 'newspack_post_registration_newsletters_lists', $registration_lists );
+ }
+
+ /**
+ * Get all available newsletter lists.
+ *
+ * @param string $email_address Email address. Optional.
+ *
+ * @return array
+ */
+ public static function get_available_newsletter_lists( $email_address = '' ) {
if ( ! method_exists( 'Newspack_Newsletters_Subscription', 'get_lists' ) ) {
return [];
}
- $use_custom_lists = self::get_setting( 'use_custom_lists' );
- $available_lists = \Newspack_Newsletters_Subscription::get_lists_config();
+ $use_custom_lists = self::get_setting( 'use_custom_lists' );
+ $available_lists = \Newspack_Newsletters_Subscription::get_lists_config();
+ $registration_lists = [];
if ( \is_wp_error( $available_lists ) ) {
return [];
}
@@ -396,7 +600,6 @@ public static function get_registration_newsletter_lists() {
if ( empty( $lists ) ) {
return [];
}
- $registration_lists = [];
foreach ( $lists as $list ) {
if ( isset( $available_lists[ $list['id'] ] ) ) {
$registration_lists[ $list['id'] ] = $available_lists[ $list['id'] ];
@@ -405,6 +608,23 @@ public static function get_registration_newsletter_lists() {
}
}
+ // Filter out any lists the reader is already signed up for if an email address is provided.
+ if ( $email_address && method_exists( '\Newspack_Newsletters_Subscription', 'get_contact_lists' ) ) {
+ $current_lists = \Newspack_Newsletters_Subscription::get_contact_lists( $email_address );
+ if ( ! \is_wp_error( $current_lists ) && is_array( $current_lists ) ) {
+ $filtered_lists = [];
+ foreach ( $registration_lists as $list ) {
+ // Skip any lists the reader is already signed up for.
+ if ( in_array( $list['id'], $current_lists, true ) ) {
+ continue;
+ }
+
+ $filtered_lists[ $list['id'] ] = $list;
+ }
+ $registration_lists = $filtered_lists;
+ }
+ }
+
/**
* Filters the newsletters lists that should be rendered during registration.
*
@@ -797,7 +1017,7 @@ public static function set_reader_verified( $user_or_user_id ) {
\update_user_meta( $user->ID, self::EMAIL_VERIFIED, true );
- WooCommerce_Connection::add_wc_notice( __( 'Thank you for verifying your account!', 'newspack-plugin' ), 'success' );
+ WooCommerce_Connection::add_wc_notice( self::get_reader_activation_labels( 'verify' ), 'success' );
/**
* Upon verification we want to destroy existing sessions to prevent a bad
@@ -845,6 +1065,28 @@ public static function set_reader_has_password( $user_or_user_id ) {
return true;
}
+ /**
+ * Conditionally remove "without password" meta from user.
+ *
+ * If the a password is being set via user profile update,
+ * And a previous password was not set, we remove the meta.
+ *
+ * @param int $user_id User ID.
+ * @param \WP_User $old_user_data Old user data.
+ * @param array $user_data User data.
+ */
+ public static function maybe_set_reader_has_password( $user_id, $old_user_data, $user_data ) {
+ if ( ! self::is_user_reader( $old_user_data ) ) {
+ return;
+ }
+
+ $old_password = $old_user_data->user_pass;
+ $new_password = isset( $user_data['user_pass'] ) ? $user_data['user_pass'] : '';
+ if ( ! empty( $new_password ) && $old_password !== $new_password ) {
+ self::set_reader_has_password( $user_id );
+ }
+ }
+
/**
* Whether the reader hasn't set its own password.
*
@@ -998,33 +1240,6 @@ public static function nav_menu_items( $output, $args = [] ) {
return $output;
}
- /**
- * Get the account icon SVG markup.
- *
- * @return string The account icon SVG markup.
- */
- private static function get_account_icon() {
- return '';
- }
-
- /**
- * Get the error icon SVG markup.
- *
- * @return string The error icon SVG markup.
- */
- private static function get_error_icon() {
- return '';
- }
-
- /**
- * Get the check icon SVG markup.
- *
- * @return string The check icon SVG markup.
- */
- private static function get_check_icon() {
- return '';
- }
-
/**
* Get account link.
*
@@ -1046,15 +1261,13 @@ private static function get_account_link() {
return self::get_element_class_name( $parts );
};
- $labels = [
- 'signedin' => \__( 'My Account', 'newspack-plugin' ),
- 'signedout' => \__( 'Sign In', 'newspack-plugin' ),
- ];
+ $labels = self::get_reader_activation_labels( 'account_link' );
$label = \is_user_logged_in() ? 'signedin' : 'signedout';
+ $href = \is_user_logged_in() ? $account_url : '#';
- $link = '';
+ $link = '';
$link .= '';
- $link .= self::get_account_icon();
+ $link .= \Newspack\Newspack_UI_Icons::get_svg( 'account' );
$link .= '';
$link .= '' . \esc_html( $labels[ $label ] ) . '';
$link .= '';
@@ -1092,9 +1305,9 @@ public static function render_honeypot_field( $placeholder = '' ) {
/**
* Renders reader authentication form.
*
- * @param boolean $is_inline If true, render the form inline, otherwise render as a modal.
+ * @param boolean $in_modal Whether the form is rendiner in a modal; defaults to true.
*/
- public static function render_auth_form( $is_inline = false ) {
+ public static function render_auth_form( $in_modal = true ) {
/**
* Filters whether to render reader auth form.
*
@@ -1108,206 +1321,220 @@ public static function render_auth_form( $is_inline = false ) {
return;
}
- $class = function( ...$parts ) {
- array_unshift( $parts, 'auth-form' );
- return self::get_element_class_name( $parts );
- };
-
- $labels = [
- 'signin' => \__( 'Sign In', 'newspack-plugin' ),
- 'register' => \__( 'Sign Up', 'newspack-plugin' ),
- ];
-
- $message = '';
- $classnames = [ 'newspack-reader-auth', $class() ];
+ $message = '';
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['reader_authenticated'] ) && isset( $_GET['message'] ) ) {
- $message = \sanitize_text_field( $_GET['message'] );
- $classnames[] = $class( 'visible' );
+ $message = \sanitize_text_field( $_GET['message'] );
}
// phpcs:enable
- if ( $is_inline ) {
- $classnames[] = $class( 'inline' );
+ $referer = \wp_parse_url( \wp_get_referer() );
+ $labels = self::get_reader_activation_labels( 'signin' );
+ $auth_callback_url = '#';
+ // If we are already on the my account page, set the my account URL so the page reloads on submit.
+ if ( function_exists( 'wc_get_page_permalink' ) && function_exists( 'is_account_page' ) && \is_account_page() ) {
+ $auth_callback_url = \wc_get_page_permalink( 'myaccount' );
}
-
- $newsletters_label = self::get_setting( 'newsletters_label' );
- if ( method_exists( 'Newspack_Newsletters_Subscription', 'get_lists_config' ) ) {
- $lists_config = self::get_registration_newsletter_lists();
- if ( ! \is_wp_error( $lists_config ) ) {
- $lists = $lists_config;
- }
- }
- $terms_text = self::get_setting( 'terms_text' );
- $terms_url = self::get_setting( 'terms_url' );
- $is_account_page = function_exists( '\wc_get_page_id' ) ? \get_the_ID() === \wc_get_page_id( 'myaccount' ) : false;
- $redirect = $is_account_page ? \wc_get_account_endpoint_url( 'dashboard' ) : '';
- $referer = \wp_parse_url( \wp_get_referer() );
- global $wp;
?>
- Component Demo+ +
+ + Typography+ +2X-Small text +X-Small text +Small text (default) +Medium text +Large text +X-Large text +2X-Large text +3X-Large text +4X-Large text +5X-Large text +6X-Large text + ++ + Boxes+ +
+
+
+ Default box style +
+
+
+ Border box style +
+
+
+ "Success" box style +
+
+
+
+
+
+
+ Success box style, plus icon + Plus a little bit of text below it. +
+
+
+
+
+
+
+ Warning box style, plus icon + Plus a little bit of text below it. +
+
+
+
+
+
+
+ Error box style, plus icon + Plus a little bit of text below it. ++ + Notices+ +
+
+
+
+
+ Default notice style +
+
+
+
+
+ "Success" notice style +
+
+
+
+
+ "Warning" notice style +
+
+
+
+
+ "Error" notice style +
+
+
+
+
+
+ Default notice with icon style +
+
+
+
+
+
+ "Success" notice with icon style +
+
+
+
+
+
+ "Warning" notice with icon style +
+
+
+
+
+
+ "Error" notice with icon style ++ + Form elements+ + + ++ + Checkbox/Radio Lists+ + + + + ++ + + + + + + + + + + + + + + + Order table+Transaction details+
+
+
+
+ + Badges+ Badge+ Badge + Badge + Badge + Badge + Badge + + + + + Buttons+
+ + + + + + + + + + + + + + Wide buttons+ + + + + + ++ + Uses the + + + + + + + + + + + + + + + + + + + Segmented Controls+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + Buttons Icon+ +Uses the same classes as the Uses the + + Modals+ +
+
+
+
+
+
+ This is a header+ +This is the modal content +Small size+ +
+
+
+
+
+ Auth Modal Contents Default+ + +
+ Or
+
+
+
+
+
+
+
+
+ Auth Modal Contents OTP+ + +
+
+
+
+
+
+ Auth Modal Contents Success+ + +
+
+
+
+
+
+
+
+
+ + Success! Your account was created and you're signed in. + + +In the future, you'll sign in with a code sent to your email. If you'd rather use a password, you can set one in My Account. +
+
+
+
+
+
+ Auth Modal Contents Success + PW+ + +
+
+
+
+
+
+
+
+ + Success! Your account was created and you're signed in. + +
+
+
+
+
+ Auth Modal Newsletter Sign Up+ + +Get the best of The News Paper directly to your email inbox.
+
+
+
+
+
+ Change Subscription+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auth Modal Contents Default+ + +
+ Or
+
+
+
+
-
-
-
+
+
+
+
+
+
{ editedState === 'initial' && (
-
+
+
+
+
+ + + + +
-
>
diff --git a/src/blocks/reader-registration/editor.scss b/src/blocks/reader-registration/editor.scss
index 5ad6410b51..a4c67c480d 100644
--- a/src/blocks/reader-registration/editor.scss
+++ b/src/blocks/reader-registration/editor.scss
@@ -1,70 +1,15 @@
@use "./style";
-@use "../../reader-activation/auth";
+@use "../../reader-activation-auth/style" as auth;
@use "~@wordpress/base-styles/colors" as wp-colors;
.newspack-registration {
- font-size: inherit;
-
- form {
- padding-top: 0;
-
- input[type="email"] {
- background: #fff;
- border: solid 1px #ccc;
- box-sizing: border-box;
- font-size: inherit;
- outline: none;
- padding: 0.36rem 0.66rem;
- appearance: none;
- outline-offset: 0;
- border-radius: 0;
- }
- [type="submit"] {
- transition: background-color 150ms ease-in-out;
- background-color: #d33;
- border: none;
- border-radius: 5px;
- box-sizing: border-box;
- display: inline-block;
- color: #fff;
- font-family:
- -apple-system,
- blinkmacsystemfont,
- "Segoe UI",
- Roboto,
- Oxygen,
- Ubuntu,
- Cantarell,
- "Fira Sans",
- "Droid Sans",
- "Helvetica Neue",
- sans-serif;
- font-size: 0.8em;
- font-weight: 700;
- line-height: 1.2;
- outline: none;
- padding: 0.76rem 1rem;
- text-decoration: none;
- vertical-align: bottom;
- }
- }
&__state-bar {
align-items: center;
+ background: white;
border: 1px solid wp-colors.$gray-900;
border-radius: 2px;
- display: flex;
- font-family:
- -apple-system,
- blinkmacsystemfont,
- "Segoe UI",
- Roboto,
- Oxygen,
- Ubuntu,
- Cantarell,
- "Fira Sans",
- "Droid Sans",
- "Helvetica Neue",
- sans-serif;
+ display: none;
+ font-family: system-ui, sans-serif;
flex-wrap: wrap;
font-size: 13px;
gap: 8px;
@@ -73,11 +18,13 @@
margin-bottom: 12px;
padding: 7px 8px;
button {
+ background: none !important;
+ border-radius: 2px !important;
box-shadow: none !important;
color: wp-colors.$gray-900 !important;
height: 32px !important;
&[data-is-active="true"] {
- background-color: wp-colors.$gray-900;
+ background: wp-colors.$gray-900 !important;
color: white !important;
}
&:hover:not([data-is-active="true"]) {
@@ -92,85 +39,130 @@
gap: 4px;
}
}
- & &__header {
- font-size: 0.8em;
- h2 {
- font-size: 1rem;
- line-height: 1.3333;
+ & &__title {
+ font-family: var(--newspack-ui-font-family);
+ }
- @media screen and ( min-width: 600px ) {
- font-size: 1.3125em;
- line-height: 1.5238;
+ &__main {
+ gap: 0;
+
+ button[class*="oauth"] {
+ svg {
+ display: block;
}
}
+ }
- p {
- font-size: 1em;
+ &__form-content {
+ &:not(:has(.newspack-ui__helper-text)) {
+ .newspack-ui__input-card {
+ border-bottom-width: 0 !important;
+ border-radius: 0 !important;
+ border-top-width: 0 !important;
+
+ strong {
+ font-weight: 400;
+ }
+
+ &:has(input[type="checkbox"]:checked) {
+ background: transparent;
+ border-color: var(--newspack-ui-color-border);
+ }
+
+ &:first-of-type {
+ border-top-left-radius: var(--newspack-ui-border-radius-m) !important;
+ border-top-right-radius: var(--newspack-ui-border-radius-m) !important;
+ border-top-width: 1px !important;
+ }
+
+ &:last-of-type {
+ border-bottom-left-radius: var(--newspack-ui-border-radius-m) !important;
+ border-bottom-right-radius: var(--newspack-ui-border-radius-m) !important;
+ border-bottom-width: 1px !important;
+ }
+
+ + .newspack-ui__input-card {
+ margin-top: calc(-1 * var(--newspack-ui-spacer-2)) !important;
+ padding-top: 0 !important;
+ }
+ }
}
}
- &__help-text {
- margin-top: 1em;
- p {
- font-size: 0.55em !important;
- margin: 0;
+ &__inputs,
+ &__have-account {
+ .newspack-ui__button {
+ cursor: default;
+ font-family: var(--newspack-ui-font-family) !important;
+
+ &:focus {
+ box-shadow: none;
+ outline: unset;
+ }
+ }
+
+ .rich-text [data-rich-text-placeholder] {
+ width: 100%;
+
+ &::after {
+ opacity: 1;
+ }
}
}
+
+ &__help-text {
+ .rich-text {
+ font-size: var(--newspack-ui-font-size-xs);
+ line-height: var(--newspack-ui-line-height-xs);
+ }
+ }
+
&__response {
display: none;
}
- &__icon {
- + .block-editor-block-list__layout p,
- + .block-editor-rich-text__editable {
- font-size: 0.8em;
- }
- }
-}
-.newspack-reader {
- &__logins {
- button {
- padding: 0.76rem 1rem;
- border: none;
- border-radius: 5px;
- font-size: 0.8em;
- font-family: sans-serif;
- font-weight: 700;
- }
+ button:focus,
+ a:focus {
+ outline: none !important;
}
-}
-.wp-block-newspack-reader-registration {
- .block-editor-block-list__layout {
- margin: 0 auto;
- max-width: 780px;
+ .newspack-ui__box--success {
+ font-size: var(--newspack-ui-font-size-s);
+ font-weight: 600;
+ line-height: var(--newspack-ui-line-height-s);
+ margin: 0 !important;
.block-list-appender {
position: relative;
+ width: 100%;
}
- }
- .newspack-registration {
- div {
- a {
- color: inherit;
- cursor: pointer;
- text-decoration: underline;
-
- &:active,
- &:focus,
- &:hover {
- color: inherit;
- text-decoration: none;
- }
- }
+ p {
+ font-size: inherit;
+ font-weight: inherit;
+ line-height: inherit;
}
- form {
- > p {
- font-size: 0.8em;
- }
+ > * {
+ margin-bottom: var(--newspack-ui-spacer-2);
+ margin-top: var(--newspack-ui-spacer-2);
+ }
+
+ > :first-child {
+ margin-top: 0;
+ }
+
+ > :last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.wp-block-newspack-reader-registration {
+ &.is-selected {
+ .newspack-registration__state-bar {
+ display: flex;
}
}
}
diff --git a/src/blocks/reader-registration/index.php b/src/blocks/reader-registration/index.php
index 81b83fee91..9b4b906927 100644
--- a/src/blocks/reader-registration/index.php
+++ b/src/blocks/reader-registration/index.php
@@ -31,22 +31,6 @@ function register_block() {
if ( ! Reader_Activation::is_enabled() ) {
return;
}
-
- \register_block_style(
- 'newspack/reader-registration',
- [
- 'name' => 'stacked',
- 'label' => __( 'Stacked', 'newspack-plugin' ),
- 'is_default' => true,
- ]
- );
- \register_block_style(
- 'newspack/reader-registration',
- [
- 'name' => 'columns',
- 'label' => __( 'Columns (newsletter subscription)', 'newspack-plugin' ),
- ]
- );
}
add_action( 'init', __NAMESPACE__ . '\\register_block' );
@@ -112,7 +96,7 @@ function render_block( $attrs, $content ) {
$registered = false;
$my_account_url = function_exists( 'wc_get_account_endpoint_url' ) ? \wc_get_account_endpoint_url( 'dashboard' ) : false;
$message = '';
- $success_message = __( 'Thank you for registering!', 'newspack-plugin' ) . '
-
+
+
) }
{ editedState === 'registration' && (
- <>
-
-
- >
+
{ ! shouldHideSubscribeInput() && newsletterSubscription && lists.length ? (
-
+
+
- { lists?.length > 1 && (
-
+ ) }
+
+ ) ) }
+ >
) : null }
+ { newspack_blocks.has_google_oauth && (
+
+
+
+ ) }
+ { __( 'Or', 'newspack-plugin' ) }
+
+
-
-
- { newspack_blocks.has_google_oauth && (
-
-
- ) }
-
-
- { __( 'OR', 'newspack-plugin' ) }
-
-
-
+
+
) }
{ editedState === 'login' && (
- <>
-
-
+
+
+
+
+
+
+
) }
+
+
+
+
+ '; + $success_message = __( 'Success! Your account was created and you’re signed in.', 'newspack-plugin' ) . ' '; if ( $my_account_url ) { $success_message .= sprintf( @@ -124,12 +108,10 @@ function render_block( $attrs, $content ) { /** Handle default attributes. */ $default_attrs = [ - 'style' => 'stacked', - 'label' => __( 'Sign up', 'newspack-plugin' ), - 'newsletterLabel' => __( 'Subscribe to our newsletter', 'newspack-plugin' ), - 'haveAccountLabel' => __( 'Already have an account?', 'newspack-plugin' ), - 'signInLabel' => __( 'Sign in', 'newspack-plugin' ), - 'signedInLabel' => __( 'An account was already registered with this email. Please check your inbox for an authentication link.', 'newspack-plugin' ), + 'label' => __( 'Sign up', 'newspack-plugin' ), + 'newsletterLabel' => __( 'Subscribe to our newsletter', 'newspack-plugin' ), + 'signInLabel' => __( 'Sign in to an existing account', 'newspack-plugin' ), + 'signedInLabel' => __( 'An account was already registered with this email. Please check your inbox for an authentication link.', 'newspack-plugin' ), ]; $attrs = \wp_parse_args( $attrs, $default_attrs ); foreach ( $default_attrs as $key => $value ) { @@ -179,12 +161,12 @@ function render_block( $attrs, $content ) { $success_registration_markup = $content; if ( empty( \wp_strip_all_tags( $content ) ) ) { - $success_registration_markup = ' ' . $success_message . ' '; + $success_registration_markup = '' . $success_message . ' '; } $success_login_markup = $attrs['signedInLabel']; if ( ! empty( \wp_strip_all_tags( $attrs['signedInLabel'] ) ) ) { - $success_login_markup = '' . $attrs['signedInLabel'] . ' '; + $success_login_markup = '' . $attrs['signedInLabel'] . ' '; } $checked = []; @@ -198,25 +180,23 @@ function render_block( $attrs, $content ) { ob_start(); ?> -
+
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
@@ -250,7 +230,6 @@ function render_block( $attrs, $content ) {
$lists,
$checked,
[
- 'title' => $attrs['newsletterTitle'],
'single_label' => $attrs['newsletterLabel'],
'show_description' => $attrs['displayListDescription'],
]
@@ -260,9 +239,7 @@ function render_block( $attrs, $content ) {
?>
-
-
-
+
-
-
type="submit"
+ class="newspack-ui__button newspack-ui__button--primary"
>
-
-
- - - - - - - -
+
+
+
+
+
+
- + + + + + + + +
-
+
+
+
+
-
-
+
-
-
:first-child {
+ margin-top: 0;
}
- }
- &__response {
- margin-top: 0.5rem;
- font-size: 0.8em;
+ > :last-child {
+ margin-bottom: 0;
+ }
}
+ &__login-success,
&__registration-success {
- > *:last-child {
- margin-bottom: 0;
- }
+ font-weight: 600;
+ margin: 0 !important;
}
&--error {
.newspack-registration__response {
- color: wp-colors.$alert-red;
+ color: var(--newspack-ui-color-error-50);
+ font-size: var(--newspack-ui-font-size-xs);
+ line-height: var(--newspack-ui-line-height-xs);
+
+ p {
+ margin: var(--newspack-ui-spacer-base) 0 0;
+ }
+ }
+
+ .newspack-registration__inputs {
+ input[type="email"] {
+ border-color: var(--newspack-ui-color-error-50);
+
+ &:focus {
+ outline-color: var(--newspack-ui-color-error-50);
+ }
+ }
}
}
@@ -156,11 +194,37 @@
}
}
+ .newspack-ui__box--success {
+ .block-editor-block-list__layout,
+ .wp-block-newspack-reader-registration {
+ font-weight: 600;
+ margin: var(--newspack-ui-spacer-2) 0;
+
+ > * {
+ margin-bottom: var(--newspack-ui-spacer-2);
+ margin-top: var(--newspack-ui-spacer-2);
+ }
+
+ > :first-child {
+ margin-top: 0;
+ }
+
+ > :last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .wp-block-list {
+ text-align: initial;
+ }
+ }
+
&--in-progress {
- opacity: 0.5;
- button,
- a,
- input {
+ body:has(&) {
+ cursor: wait;
+ }
+
+ * {
pointer-events: none;
}
@@ -193,61 +257,7 @@
}
}
- &__icon {
- align-items: center;
- animation: fadein 125ms ease-in;
- background: wp-colors.$alert-green;
- border-radius: 50%;
- display: flex;
- height: 40px;
- justify-content: center;
- margin: 0 auto 0.8rem;
- width: 40px;
-
- &::before {
- animation: bounce 125ms ease-in;
- animation-delay: 500ms;
- animation-fill-mode: forwards;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z' fill='white'/%3E%3C/svg%3E");
- content: "";
- display: block;
- height: 24px;
- transform: scale(0);
- width: 24px;
- }
- }
-
- &__success {
- margin: 0 auto;
- max-width: 780px;
-
- &:not([class*="--hidden"]) {
- .newspack-registration__icon {
- animation: fadein 125ms ease-in;
-
- &::before {
- animation: bounce 125ms ease-in;
- animation-delay: 500ms;
- animation-fill-mode: forwards;
- }
- }
- }
-
- .wp-block-newspack-reader-registration {
- > *:first-child {
- margin-top: 0 !important;
- }
-
- > *:last-child {
- margin-bottom: 0 !important;
- }
- }
- }
-
- &--hidden {
- display: none;
- }
-
+ &--hidden,
+ div:empty {
display: none;
}
@@ -258,6 +268,13 @@
}
}
+@container registration ( width > 568px ) {
+ .newspack-registration__inputs {
+ gap: var(--newspack-ui-spacer-2);
+ grid-template-columns: 1fr auto;
+ }
+}
+
/* Sepcific styles to apply to Reader Registration when part of a Prompt */
.newspack-popup__content {
.newspack-registration__header {
@@ -269,24 +286,12 @@
}
}
-@keyframes fadein {
+@keyframes spin {
from {
- opacity: 0;
+ transform: rotate(0deg);
}
to {
- opacity: 1;
- }
-}
-
-@keyframes bounce {
- 0% {
- transform: scale(0);
- }
- 90% {
- transform: scale(1.4);
- }
- 100% {
- transform: scale(1);
+ transform: rotate(360deg);
}
}
diff --git a/src/blocks/reader-registration/view.js b/src/blocks/reader-registration/view.js
index 17937f5d4a..7e91e6d6c1 100644
--- a/src/blocks/reader-registration/view.js
+++ b/src/blocks/reader-registration/view.js
@@ -60,7 +60,6 @@ window.newspackRAS.push( function( readerActivation ) {
if ( message ) {
messageNode = document.createElement( 'p' );
- messageNode.classList.add( 'has-text-align-center' );
messageNode.textContent = message;
const defaultMessage = successElement.querySelector( 'p' );
@@ -83,7 +82,9 @@ window.newspackRAS.push( function( readerActivation ) {
messageElement.appendChild( messageNode );
messageElement.classList.remove( 'newspack-registration--hidden' );
}
- submitElement.removeChild( spinner );
+ if ( submitElement.contains( spinner ) ) {
+ submitElement.removeChild( spinner );
+ }
submitElement.disabled = false;
container.classList.remove( 'newspack-registration--in-progress' );
};
@@ -96,46 +97,24 @@ window.newspackRAS.push( function( readerActivation ) {
return form.endLoginFlow( 'Please enter a vaild email address.', 400 );
}
- readerActivation
- .getCaptchaV3Token() // Get a token for reCAPTCHA v3, if needed.
- .then( captchaToken => {
- // If there's no token, we don't need to do anything.
- if ( ! captchaToken ) {
- return;
- }
- let tokenField = form[ 'g-recaptcha-response' ];
- if ( ! tokenField ) {
- tokenField = document.createElement( 'input' );
- tokenField.setAttribute( 'type', 'hidden' );
- tokenField.setAttribute( 'name', 'g-recaptcha-response' );
- tokenField.setAttribute( 'autocomplete', 'off' );
- form.appendChild( tokenField );
- }
- tokenField.value = captchaToken;
+ const body = new FormData( form );
+ if ( ! body.has( 'npe' ) || ! body.get( 'npe' ) ) {
+ return form.endFlow( 'Please enter a vaild email address.', 400 );
+ }
+ fetch( form.getAttribute( 'action' ) || window.location.pathname, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ },
+ body,
+ } )
+ .then( res => {
+ res
+ .json()
+ .then( ( { message, data } ) => form.endLoginFlow( message, res.status, data ) );
} )
.catch( e => {
form.endLoginFlow( e, 400 );
- } )
- .finally( () => {
- const body = new FormData( form );
- if ( ! body.has( 'npe' ) || ! body.get( 'npe' ) ) {
- return form.endFlow( 'Please enter a vaild email address.', 400 );
- }
- fetch( form.getAttribute( 'action' ) || window.location.pathname, {
- method: 'POST',
- headers: {
- Accept: 'application/json',
- },
- body,
- } )
- .then( res => {
- res
- .json()
- .then( ( { message, data } ) => form.endLoginFlow( message, res.status, data ) );
- } )
- .catch( e => {
- form.endLoginFlow( e, 400 );
- } );
} );
} );
diff --git a/src/newspack-ui/fonts/Inter-VariableFont_slnt,wght.ttf b/src/newspack-ui/fonts/Inter-VariableFont_slnt,wght.ttf
new file mode 100644
index 0000000000..e72470871b
Binary files /dev/null and b/src/newspack-ui/fonts/Inter-VariableFont_slnt,wght.ttf differ
diff --git a/src/newspack-ui/fonts/InterVariable-Italic.woff2 b/src/newspack-ui/fonts/InterVariable-Italic.woff2
new file mode 100644
index 0000000000..f22ec25549
Binary files /dev/null and b/src/newspack-ui/fonts/InterVariable-Italic.woff2 differ
diff --git a/src/newspack-ui/fonts/InterVariable.woff2 b/src/newspack-ui/fonts/InterVariable.woff2
new file mode 100644
index 0000000000..22a12b04e1
Binary files /dev/null and b/src/newspack-ui/fonts/InterVariable.woff2 differ
diff --git a/src/newspack-ui/fonts/LICENSE.md b/src/newspack-ui/fonts/LICENSE.md
new file mode 100644
index 0000000000..9b2ca37b3f
--- /dev/null
+++ b/src/newspack-ui/fonts/LICENSE.md
@@ -0,0 +1,92 @@
+Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION AND CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/newspack-ui/fonts/OFL.txt b/src/newspack-ui/fonts/OFL.txt
new file mode 100644
index 0000000000..9b2ca37b3f
--- /dev/null
+++ b/src/newspack-ui/fonts/OFL.txt
@@ -0,0 +1,92 @@
+Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION AND CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/newspack-ui/index.js b/src/newspack-ui/index.js
new file mode 100644
index 0000000000..72c8d03a0b
--- /dev/null
+++ b/src/newspack-ui/index.js
@@ -0,0 +1,2 @@
+import './style.scss';
+import './js/index.js';
diff --git a/src/newspack-ui/js/index.js b/src/newspack-ui/js/index.js
new file mode 100644
index 0000000000..1075a5b952
--- /dev/null
+++ b/src/newspack-ui/js/index.js
@@ -0,0 +1 @@
+import './segmented-control';
diff --git a/src/newspack-ui/js/segmented-control/index.js b/src/newspack-ui/js/segmented-control/index.js
new file mode 100644
index 0000000000..ef1d2103de
--- /dev/null
+++ b/src/newspack-ui/js/segmented-control/index.js
@@ -0,0 +1,34 @@
+import { domReady } from '../utils'; // Global utils.
+
+domReady( function () {
+ function segmented_control( element ) {
+ const header = element.querySelector( '.newspack-ui__segmented-control__tabs' );
+ const tab_headers = [ ...header.children ];
+ const tab_body = element.querySelector( '.newspack-ui__segmented-control__content' );
+ let tab_contents = [];
+
+ if ( null !== tab_body ) {
+ tab_contents = [ ...tab_body.children ];
+ }
+
+ tab_headers.forEach( ( tab, i ) => {
+ if ( tab_contents.length !== 0 && tab.classList.contains( 'selected' ) ) {
+ tab_contents[ i ].classList.add( 'selected' );
+ }
+
+ tab.addEventListener( 'click', function () {
+ tab_headers.forEach( t => t.classList.remove( 'selected' ) );
+ this.classList.add( 'selected' );
+
+ if ( tab_contents.length !== 0 ) {
+ tab_contents.forEach( content => content.classList.remove( 'selected' ) );
+ tab_contents[ i ].classList.add( 'selected' );
+ }
+ } );
+ } );
+ }
+
+ [ ...document.querySelectorAll( '.newspack-ui__segmented-control' ) ].forEach( x =>
+ segmented_control( x )
+ );
+} );
diff --git a/src/newspack-ui/js/utils/index.js b/src/newspack-ui/js/utils/index.js
new file mode 100644
index 0000000000..17c2b92274
--- /dev/null
+++ b/src/newspack-ui/js/utils/index.js
@@ -0,0 +1,25 @@
+/**
+ * Util functions.
+ */
+
+/**
+ * Specify a function to execute when the DOM is fully loaded.
+ *
+ * @see https://github.com/WordPress/gutenberg/blob/trunk/packages/dom-ready/
+ *
+ * @param {Function} callback A function to execute after the DOM is ready.
+ * @return {void}
+ */
+export const domReady = callback => {
+ if ( typeof document === 'undefined' ) {
+ return;
+ }
+ if (
+ document.readyState === 'complete' || // DOMContentLoaded + Images/Styles/etc loaded, so we call directly.
+ document.readyState === 'interactive' // DOMContentLoaded fires at this point, so we call directly.
+ ) {
+ return void callback();
+ }
+ // DOMContentLoaded has not fired yet, delay callback until then.
+ document.addEventListener( 'DOMContentLoaded', callback );
+};
diff --git a/src/newspack-ui/scss/_mixins.scss b/src/newspack-ui/scss/_mixins.scss
new file mode 100644
index 0000000000..e39d0e7307
--- /dev/null
+++ b/src/newspack-ui/scss/_mixins.scss
@@ -0,0 +1,33 @@
+@use "variables/breakpoints";
+
+@mixin newspack-ui-elevation( $size: 1 ) {
+ @if $size == 1 {
+ box-shadow: 0 1px 10px rgba(0, 0, 0, 0.07);
+ }
+ @else if $size == 2 {
+ box-shadow: 0 2px 20px rgba(0, 0, 0, 0.14);
+ }
+ @else if $size == 3 {
+ box-shadow: 0 3px 30px rgba(0, 0, 0, 0.21);
+ }
+}
+
+@mixin media( $res ) {
+ @if mobile == $res {
+ @media only screen and ( min-width: breakpoints.$mobile_width ) {
+ @content;
+ }
+ }
+
+ @if tablet == $res {
+ @media only screen and ( min-width: breakpoints.$tablet_width ) {
+ @content;
+ }
+ }
+
+ @if desktop == $res {
+ @media only screen and ( min-width: breakpoints.$desktop_width ) {
+ @content;
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/_modals.scss b/src/newspack-ui/scss/_modals.scss
new file mode 100644
index 0000000000..6387e77e20
--- /dev/null
+++ b/src/newspack-ui/scss/_modals.scss
@@ -0,0 +1,138 @@
+@use "mixins";
+
+.newspack-ui {
+ &__modal-container {
+ position: fixed;
+ z-index: -1;
+ inset: 0;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ visibility: hidden;
+ pointer-events: none;
+ &__overlay {
+ position: absolute;
+ z-index: 1;
+ inset: 0;
+ opacity: 0;
+ background: rgba(0, 0, 0, 0.7);
+ transition: opacity 0.125s linear;
+ }
+ .newspack-ui__modal {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ transform: translateY(50px);
+ opacity: 0;
+ transition: transform 0.125s linear, opacity 0.125s linear;
+ @include mixins.newspack-ui-elevation( 3 );
+ }
+ &[data-state="open"] {
+ z-index: 99999;
+ visibility: visible;
+ pointer-events: auto;
+ .newspack-ui__modal-container__overlay {
+ opacity: 1;
+ }
+ .newspack-ui__modal {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+ }
+
+ &__modal {
+ // Modal styles
+ background: var(--newspack-ui-color-body-bg);
+ border-radius: var(--newspack-ui-border-radius-m);
+ display: flex;
+ flex-direction: column;
+ max-height: 90%;
+ max-width: var(--newspack-ui-modal-width-m);
+ overflow: auto;
+
+ // Modal header & footer
+ &__header,
+ &__footer {
+ padding: var(--newspack-ui-spacer-4) var(--newspack-ui-spacer-5);
+ }
+
+ &__header {
+ align-items: center;
+ background: var(--newspack-ui-color-body-bg);
+ border-bottom: 1px solid var(--newspack-ui-color-border);
+ color: var(--newspack-ui-color-neutral-90);
+ display: flex;
+ justify-content: space-between;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+
+ h2 {
+ color: inherit;
+ font-family: var(--newspack-ui-font-family);
+ font-size: var(--newspack-ui-font-size-s);
+ line-height: var(--newspack-ui-line-height-s);
+ margin: 0;
+ }
+ }
+
+ &__close {
+ margin: -6px -6px -6px 0 !important; // Accounts for the header padding. TODO: Can this be improved to work w/variables?
+ }
+
+ &__content {
+ backface-visibility: visible;
+ color: var(--newspack-ui-color-neutral-90);
+ padding: var(--newspack-ui-spacer-5);
+
+ // Make sure there's enough space above the first button in modals.
+ // Ignore a tag buttons as they will never be first.
+ .newspack-ui__button:not(:first-child, a.newspack-ui__button):first-of-type {
+ margin-top: var(--newspack-ui-spacer-5);
+ }
+ }
+
+ &__footer {
+ background: var(--newspack-ui-color-neutral-5);
+ color: var(--newspack-ui-color-neutral-60);
+ font-size: var(--newspack-ui-font-size-xs);
+ line-height: var(--newspack-ui-line-height-xs);
+ a {
+ text-decoration: underline;
+ }
+ }
+
+ // Narrow modal
+ &--small {
+ max-width: var(--newspack-ui-modal-width-s);
+ }
+
+ // Contents
+ h3 {
+ font-size: var(--newspack-ui-font-size-s);
+ }
+
+ &__footer,
+ &__content {
+ > *:first-child {
+ margin-top: 0;
+ }
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ strong {
+ font-weight: 600;
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/_boxes.scss b/src/newspack-ui/scss/elements/_boxes.scss
new file mode 100644
index 0000000000..d9b9eef365
--- /dev/null
+++ b/src/newspack-ui/scss/elements/_boxes.scss
@@ -0,0 +1,46 @@
+.newspack-ui {
+ .newspack-ui {
+ &__box {
+ background: var(--newspack-ui-color-neutral-5);
+ border-radius: var(--newspack-ui-border-radius-m);
+ margin-bottom: var(--newspack-ui-spacer-5);
+ padding: var(--newspack-ui-spacer-5);
+
+ // Backgrounds & Borders
+ &--success {
+ background: var(--newspack-ui-color-success-0);
+ }
+
+ &--error {
+ background: var(--newspack-ui-color-error-0);
+ }
+
+ &--warning {
+ background: var(--newspack-ui-color-warning-0);
+ }
+
+ &--border {
+ background: transparent;
+ border: 1px solid var(--newspack-ui-color-border);
+ }
+
+ // Text alignments
+ &--text-center {
+ text-align: center;
+ }
+
+ // Some style resets.
+ h2:not([class*="font-size"]) {
+ font-size: 1em;
+ }
+
+ > *:first-child {
+ margin-top: 0;
+ }
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/_icons.scss b/src/newspack-ui/scss/elements/_icons.scss
new file mode 100644
index 0000000000..38ee890b63
--- /dev/null
+++ b/src/newspack-ui/scss/elements/_icons.scss
@@ -0,0 +1,31 @@
+// Thank you SVG
+.newspack-ui {
+ &__icon {
+ align-items: center;
+ border-radius: 100%;
+ display: flex;
+ height: var(--newspack-ui-spacer-8);
+ justify-content: center;
+ margin: 0 auto var(--newspack-ui-spacer-2);
+ width: var(--newspack-ui-spacer-8);
+
+ &--success {
+ background: var(--newspack-ui-color-success-50);
+ color: var(--newspack-ui-color-body-bg);
+ }
+
+ &--error {
+ background: var(--newspack-ui-color-error-50);
+ color: var(--newspack-ui-color-body-bg);
+ }
+
+ &--warning {
+ background: var(--newspack-ui-color-warning-30);
+ color: var(--newspack-ui-color-body-bg);
+ }
+
+ svg {
+ fill: currentcolor;
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/_index.scss b/src/newspack-ui/scss/elements/_index.scss
new file mode 100644
index 0000000000..b1f8678d86
--- /dev/null
+++ b/src/newspack-ui/scss/elements/_index.scss
@@ -0,0 +1,9 @@
+@use "boxes";
+@use "notices";
+@use "forms";
+@use "icons";
+@use "misc";
+@use "segmented-control";
+@use "tables";
+@use "typography";
+@use "woocommerce";
diff --git a/src/newspack-ui/scss/elements/_notices.scss b/src/newspack-ui/scss/elements/_notices.scss
new file mode 100644
index 0000000000..80ead66c30
--- /dev/null
+++ b/src/newspack-ui/scss/elements/_notices.scss
@@ -0,0 +1,65 @@
+.newspack-ui {
+ .newspack-ui {
+ &__notice {
+ align-items: center;
+ background: var(--newspack-ui-color-neutral-5);
+ border-left-width: 3px;
+ border-left-style: solid;
+ border-left-color: var(--newspack-ui-color-neutral-90);
+ border-radius: var(--newspack-ui-border-radius-xs);
+ color: var(--newspack-ui-color-neutral-90);
+ display: flex;
+ font-size: var(--newspack-ui-font-size-xs);
+ justify-content: flex-start;
+ margin: var(--newspack-ui-spacer-6) 0;
+ padding: var(--newspack-ui-spacer-3);
+ padding-left: calc(var(--newspack-ui-spacer-3) - 3px);
+
+ > svg {
+ display: block;
+ fill: var(--newspack-ui-color-neutral-90);
+ flex: 0 0 24px;
+ margin-right: var(--newspack-ui-spacer-base);
+ }
+
+ // Backgrounds & Borders
+ &--success {
+ background: var(--newspack-ui-color-success-0);
+ border-left-color: var(--newspack-ui-color-success-50);
+
+ > svg {
+ fill: var(--newspack-ui-color-success-60);
+ }
+ }
+
+ &--warning {
+ background: var(--newspack-ui-color-warning-0);
+ border-left-color: var(--newspack-ui-color-warning-30);
+
+ > svg {
+ fill: var(--newspack-ui-color-warning-40);
+ }
+ }
+
+ &--error {
+ background: var(--newspack-ui-color-error-0);
+ border-left-color: var(--newspack-ui-color-error-50);
+
+ > svg {
+ fill: var(--newspack-ui-color-error-60);
+ }
+ }
+
+ // Some style resets.
+ > div > *:first-child {
+ margin-top: 0;
+ padding-top: 0;
+ }
+
+ > div > *:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ }
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/_segmented-control.scss b/src/newspack-ui/scss/elements/_segmented-control.scss
new file mode 100644
index 0000000000..1cc2f46175
--- /dev/null
+++ b/src/newspack-ui/scss/elements/_segmented-control.scss
@@ -0,0 +1,63 @@
+.newspack-ui {
+ &__segmented-control {
+ align-items: center;
+ display: flex;
+ gap: var(--newspack-ui-spacer-2);
+ flex-direction: column;
+
+ &__tabs {
+ background: var(--newspack-ui-color-neutral-10);
+ border-radius: var(--newspack-ui-border-radius-m);
+ display: flex;
+ flex-wrap: wrap;
+ gap: calc(var(--newspack-ui-spacer-base) / 2);
+ justify-content: center;
+ padding: calc(var(--newspack-ui-spacer-base) / 2);
+
+ .newspack-ui__button {
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: var(--newspack-ui-border-radius-xs);
+ color: var(--newspack-ui-color-neutral-60);
+ cursor: pointer;
+
+ &:hover {
+ background: transparent;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--newspack-ui-color-button-bg);
+ outline-offset: 2px;
+ }
+
+ &:not(:last-child) {
+ margin: 0;
+ }
+
+ &.selected {
+ background: var(--newspack-ui-color-neutral-0);
+ border-color: var(--newspack-ui-color-neutral-30);
+ color: var(--newspack-ui-color-neutral-90);
+ }
+ }
+ }
+
+ &__content {
+ width: 100%;
+ }
+
+ &__panel {
+ &:not(.selected) {
+ display: none;
+ }
+
+ > *:last-child {
+ margin-bottom: 0 !important;
+ }
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/_tables.scss b/src/newspack-ui/scss/elements/_tables.scss
new file mode 100644
index 0000000000..1dc130ccaf
--- /dev/null
+++ b/src/newspack-ui/scss/elements/_tables.scss
@@ -0,0 +1,29 @@
+.newspack-ui {
+ table {
+ border: 0;
+ font-family: var(--newspack-ui-font-family);
+ font-size: var(--newspack-ui-font-size-xs); // Prevent relative font size
+ line-height: var(--newspack-ui-line-height-xs);
+ margin: calc(var(--newspack-ui-spacer-base) / -2) 0; // Offset cell padding.
+ table-layout: fixed;
+ th,
+ td {
+ background: transparent;
+ border-color: var(--newspack-ui-color-border);
+ border-width: 0 0 1px;
+ font-size: var(--newspack-ui-font-size-xs); // Prevent relative font size
+ padding: calc(var(--newspack-ui-spacer-base) / 2) 0;
+ vertical-align: top;
+ }
+ th {
+ font-weight: 600;
+ text-align: left;
+ }
+ tfoot tr:last-child {
+ th,
+ td {
+ border-bottom: 0;
+ }
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/_typography.scss b/src/newspack-ui/scss/elements/_typography.scss
new file mode 100644
index 0000000000..bc8f38b814
--- /dev/null
+++ b/src/newspack-ui/scss/elements/_typography.scss
@@ -0,0 +1,89 @@
+.newspack-ui {
+ color: var(--newspack-ui-color-neutral-90);
+ font-family: var(--newspack-ui-font-family);
+ font-size: var(--newspack-ui-font-size-s);
+ line-height: var(--newspack-ui-line-height-s);
+ -webkit-font-smoothing: antialiased;
+
+ p {
+ font-size: inherit;
+ line-height: inherit;
+ margin: var(--newspack-ui-spacer-2) 0;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ font-family: var(--newspack-ui-font-family);
+ font-weight: var(--newspack-ui-font-weight-strong);
+ letter-spacing: unset;
+ text-transform: unset;
+ }
+
+ strong,
+ &__font--bold {
+ font-weight: var(--newspack-ui-font-weight-strong);
+ }
+
+ &__font--normal {
+ font-weight: normal;
+ }
+
+ & &__font--2xs {
+ font-size: var(--newspack-ui-font-size-2xs);
+ line-height: var(--newspack-ui-line-height-2xs);
+ }
+
+ & &__font--xs {
+ font-size: var(--newspack-ui-font-size-xs);
+ line-height: var(--newspack-ui-line-height-xs);
+ }
+
+ & &__font--s {
+ font-size: var(--newspack-ui-font-size-s);
+ line-height: var(--newspack-ui-line-height-s);
+ }
+
+ & &__font--m {
+ font-size: var(--newspack-ui-font-size-m);
+ line-height: var(--newspack-ui-line-height-m);
+ }
+
+ & &__font--l {
+ font-size: var(--newspack-ui-font-size-l);
+ line-height: var(--newspack-ui-line-height-l);
+ }
+
+ & &__font--xl {
+ font-size: var(--newspack-ui-font-size-xl);
+ line-height: var(--newspack-ui-line-height-xl);
+ }
+
+ & &__font--2xl {
+ font-size: var(--newspack-ui-font-size-2xl);
+ line-height: var(--newspack-ui-line-height-2xl);
+ }
+
+ & &__font--3xl {
+ font-size: var(--newspack-ui-font-size-3xl);
+ line-height: var(--newspack-ui-line-height-3xl);
+ }
+
+ & &__font--4xl {
+ font-size: var(--newspack-ui-font-size-4xl);
+ line-height: var(--newspack-ui-line-height-4xl);
+ }
+
+ & &__font--5xl {
+ font-size: var(--newspack-ui-font-size-5xl);
+ line-height: var(--newspack-ui-line-height-5xl);
+ }
+
+ & &__font--6xl {
+ font-size: var(--newspack-ui-font-size-6xl);
+ line-height: var(--newspack-ui-line-height-6xl);
+ }
+}
diff --git a/src/newspack-ui/scss/elements/forms/_buttons.scss b/src/newspack-ui/scss/elements/forms/_buttons.scss
new file mode 100644
index 0000000000..2cc8eb5269
--- /dev/null
+++ b/src/newspack-ui/scss/elements/forms/_buttons.scss
@@ -0,0 +1,239 @@
+.newspack-ui {
+ .newspack-ui__button {
+ align-items: center;
+ background: var(--newspack-ui-color-neutral-10);
+ border: 0;
+ border-radius: var(--newspack-ui-border-radius-m);
+ color: var(--newspack-ui-color-neutral-90);
+ cursor: pointer;
+ display: inline-flex;
+ font-family: var(--newspack-ui-font-family);
+ font-size: var(--newspack-ui-font-size-s);
+ font-style: normal;
+ font-weight: 600;
+ gap: calc(var(--newspack-ui-spacer-base) / 2);
+ letter-spacing: initial; // Override for custom styles.
+ text-transform: none; // Override for custom styles.
+ justify-content: center;
+ line-height: var(--newspack-ui-line-height-s);
+ margin-bottom: var(--newspack-ui-spacer-2);
+ min-height: var(--newspack-ui-spacer-9);
+ padding: var(--newspack-ui-spacer-2) var(--newspack-ui-spacer-5);
+ text-decoration: none;
+ transition:
+ background-color 125ms ease-in-out,
+ border-color 125ms ease-in-out,
+ outline 125ms ease-in-out;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &:hover {
+ background: var(--newspack-ui-color-neutral-30);
+ color: var(--newspack-ui-color-neutral-90);
+ }
+
+ &:disabled {
+ background: var(--newspack-ui-color-neutral-5) !important;
+ color: var(--newspack-ui-color-neutral-40) !important;
+ cursor: default;
+ pointer-events: none;
+ }
+
+ &:focus {
+ outline: none; // override theme default style.
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--newspack-ui-color-button-bg);
+ outline-offset: 1px;
+ }
+
+ // Sizes
+ &--x-small {
+ border-radius: var(--newspack-ui-border-radius-s);
+ font-size: var(--newspack-ui-font-size-xs);
+ line-height: var(--newspack-ui-line-height-xs);
+ min-height: var(--newspack-ui-spacer-6);
+ padding: calc(var(--newspack-ui-spacer-base) / 2) var(--newspack-ui-spacer-base);
+ }
+
+ &--small {
+ font-size: var(--newspack-ui-font-size-xs);
+ line-height: var(--newspack-ui-line-height-xs);
+ min-height: var(--newspack-ui-spacer-8);
+ padding: var(--newspack-ui-spacer-base) var(--newspack-ui-spacer-3);
+ }
+
+ &--medium {
+ // is the default.
+ }
+
+ // Styles
+ &--primary {
+ background: var(--newspack-ui-color-button-bg);
+ color: var(--newspack-ui-color-button-text);
+
+ &:hover {
+ background: var(--newspack-ui-color-button-bg-hover);
+ color: var(--newspack-ui-color-button-text-hover);
+ }
+
+ &:disabled {
+ background: var(--newspack-ui-color-neutral-30) !important;
+ color: var(--newspack-ui-color-neutral-0) !important;
+ }
+ }
+
+ &--branded {
+ background: var(--newspack-ui-color-primary);
+ color: var(--newspack-ui-color-against-primary);
+
+ &:hover {
+ background: color-mix(in srgb, var(--newspack-ui-color-primary) 80%, black);
+ color: var(--newspack-ui-color-against-primary);
+ }
+
+ &:focus-visible {
+ outline-color: var(--newspack-ui-color-primary);
+ }
+
+ &:disabled {
+ background: color-mix(in srgb, var(--newspack-ui-color-primary) 20%, white) !important;
+ color: var(--newspack-ui-color-against-primary) !important;
+ }
+ }
+
+ &--secondary {
+ background: var(--newspack-ui-color-neutral-10);
+ color: var(--newspack-ui-color-neutral-90);
+
+ &:hover {
+ background: var(--newspack-ui-color-neutral-30);
+ color: var(--newspack-ui-color-neutral-90);
+ }
+
+ &:disabled {
+ background: var(--newspack-ui-color-neutral-5) !important;
+ color: var(--newspack-ui-color-neutral-40) !important;
+ }
+ }
+
+ &--ghost,
+ &--outline {
+ background: transparent;
+ color: var(---newspack-ui-color-neutral-90);
+
+ &:hover {
+ background: var(--newspack-ui-color-neutral-5);
+ color: var(--newspack-ui-color-neutral-90);
+ }
+
+ &:disabled {
+ background: transparent !important;
+ color: var(--newspack-ui-color-neutral-30) !important;
+ }
+ }
+
+ &--outline {
+ border: 1px solid var(--newspack-ui-color-neutral-30);
+ padding:
+ calc(var(--newspack-ui-spacer-2) - 1px)
+ calc(var(--newspack-ui-spacer-4) - 1px);
+
+ &:hover {
+ border-color: var(--newspack-ui-color-neutral-40);
+ }
+
+ &:disabled {
+ border-color: var(--newspack-ui-color-neutral-10);
+ }
+ }
+
+ &--destructive {
+ background: var(--newspack-ui-color-error-50);
+ color: var(--newspack-ui-color-neutral-0);
+
+ &:hover {
+ background: var(--newspack-ui-color-error-60);
+ color: var(--newspack-ui-color-neutral-0);
+ }
+
+ &:focus-visible {
+ outline-color: var(--newspack-ui-color-error-50);
+ }
+
+ &:disabled {
+ background: var(--newspack-ui-color-error-5) !important;
+ color: var(--newspack-ui-color-neutral-0) !important;
+ }
+ }
+
+ // Wide
+ &--wide {
+ display: flex;
+ width: 100%;
+ }
+
+ // Icon-only
+ &--icon {
+ display: grid;
+ height: var(--newspack-ui-spacer-7);
+ min-height: unset;
+ padding: 0;
+ place-items: center;
+ width: var(--newspack-ui-spacer-7);
+
+ // Sizes
+ &.newspack-ui__button--x-small {
+ height: var(--newspack-ui-spacer-7);
+ width: var(--newspack-ui-spacer-7);
+ }
+
+ &.newspack-ui__button--small {
+ height: var(--newspack-ui-spacer-8);
+ width: var(--newspack-ui-spacer-8);
+ }
+
+ &.newspack-ui__button--medium {
+ height: var(--newspack-ui-spacer-9);
+ width: var(--newspack-ui-spacer-9);
+ }
+ }
+
+ svg {
+ fill: currentcolor;
+ }
+
+ // Loading
+ &--loading {
+ position: relative;
+
+ span {
+ visibility: hidden;
+ }
+
+ &::before {
+ animation: button-loading 900ms infinite linear;
+ border: 1.5px solid;
+ border-color: currentcolor currentcolor transparent transparent;
+ border-radius: 50%;
+ content: "";
+ display: block;
+ height: calc(var(--newspack-ui-spacer-base) * 2.25);
+ position: absolute;
+ width: calc(var(--newspack-ui-spacer-base) * 2.25);
+ }
+ }
+ }
+}
+
+@keyframes button-loading {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/newspack-ui/scss/elements/forms/_checkbox-radio.scss b/src/newspack-ui/scss/elements/forms/_checkbox-radio.scss
new file mode 100644
index 0000000000..d4e1bf2bc4
--- /dev/null
+++ b/src/newspack-ui/scss/elements/forms/_checkbox-radio.scss
@@ -0,0 +1,87 @@
+.newspack-ui {
+ input[type="checkbox"],
+ input[type="radio"] {
+ appearance: none;
+ background: white;
+ border: solid 1px var(--newspack-ui-color-input-border);
+ box-shadow: none;
+ color: white;
+ cursor: pointer;
+ display: inline-grid;
+ font: inherit;
+ height: var(--newspack-ui-spacer-4) !important;
+ margin: 0;
+ place-content: center;
+ transition:
+ background-color 125ms ease-in-out,
+ border-color 125ms ease-in-out,
+ outline 125ms ease-in-out;
+ width: var(--newspack-ui-spacer-4) !important;
+
+ &::before {
+ content: "";
+ display: block;
+ opacity: 0;
+ }
+
+ &:checked {
+ &::before {
+ opacity: 1;
+ }
+ }
+
+ &:focus {
+ outline: unset;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--newspack-ui-color-neutral-90);
+ outline-offset: 1px;
+
+ &:not(:checked) {
+ border: 0;
+ outline-offset: -1px;
+ }
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.75;
+ }
+ }
+
+ input[type="checkbox"] {
+ border-radius: var(--newspack-ui-border-radius-2xs);
+
+ &::before {
+ background:
+ transparent
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath d='M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z' fill='white'%3E%3C/path%3E%3C/svg%3E")
+ 0 0 no-repeat;
+ height: var(--newspack-ui-spacer-5);
+ margin: 0;
+ width: var(--newspack-ui-spacer-5);
+ }
+
+ &:checked {
+ background: var(--newspack-ui-color-neutral-90);
+ border-color: transparent;
+ }
+ }
+
+ input[type="radio"] {
+ border-radius: 100%;
+
+ &::before {
+ background: var(--newspack-ui-color-neutral-0);
+ border-radius: 100%;
+ height: var(--newspack-ui-spacer-base);
+ width: var(--newspack-ui-spacer-base);
+ }
+
+ &:checked {
+ background: var(--newspack-ui-color-neutral-90);
+ border-color: var(--newspack-ui-color-neutral-90);
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/forms/_index.scss b/src/newspack-ui/scss/elements/forms/_index.scss
new file mode 100644
index 0000000000..2b2ec8a55f
--- /dev/null
+++ b/src/newspack-ui/scss/elements/forms/_index.scss
@@ -0,0 +1,67 @@
+@use "buttons";
+@use "checkbox-radio";
+@use "labels";
+@use "select";
+@use "spinner";
+@use "text-inputs";
+
+// TODO: Find a better home for this stuff.
+.newspack-ui {
+ form {
+ > *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ & &__last-child {
+ margin-bottom: 0;
+ }
+
+ // Container for displaying grid of inputs.
+ & &__code-input {
+ display: flex;
+ gap: var(--newspack-ui-spacer-base);
+ input {
+ text-align: center;
+ }
+ }
+
+ // Form inline text - helper and error.
+ &__helper-text,
+ &__inline-error {
+ color: var(--newspack-ui-color-neutral-60);
+ display: block;
+ font-size: var(--newspack-ui-font-size-xs);
+ font-weight: normal;
+ line-height: var(--newspack-ui-line-height-xs);
+ margin: var(--newspack-ui-spacer-base) 0 0;
+ a {
+ text-decoration: underline;
+ }
+
+ & + & {
+ margin-top: 0;
+ }
+
+ label & {
+ margin: 0;
+ }
+ }
+
+ &__inline-error {
+ color: var(--newspack-ui-color-error-50);
+ }
+
+ // Error class to apply to specific fields.
+ &__field-error,
+ [data-form-status="400"] {
+ // Attribute added to sign-up modal form on failure.
+ --newspack-ui-color-input-border: var(--newspack-ui-color-error-50);
+ --newspack-ui-color-input-border-focus: var(--newspack-ui-color-error-50);
+ --newspack-ui-label-color: var(--newspack-ui-color-error-50);
+
+ .newspack-ui__label-optional {
+ color: inherit;
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/forms/_labels.scss b/src/newspack-ui/scss/elements/forms/_labels.scss
new file mode 100644
index 0000000000..cd97ff11ea
--- /dev/null
+++ b/src/newspack-ui/scss/elements/forms/_labels.scss
@@ -0,0 +1,72 @@
+.newspack-ui {
+ label {
+ color: var(--newspack-ui-label-color);
+ display: block;
+ font-family: var(--newspack-ui-font-family);
+ font-size: var(--newspack-ui-font-size-s);
+ font-weight: 600;
+ line-height: var(--newspack-ui-line-height-s);
+ margin: 0 0 var(--newspack-ui-spacer-base);
+
+ // For labels containing radio, checkbox inputs:
+ &:has(input[type="checkbox"]),
+ &:has(input[type="radio"]) {
+ display: grid !important;
+ gap: 0 var(--newspack-ui-spacer-base);
+ grid-template-columns: var(--newspack-ui-spacer-4) 1fr;
+
+ > *:not(input) {
+ grid-column: 2 / span 1;
+ }
+
+ input {
+ margin-top: 0.125em; // TODO: improve the way alignment is being achieved here.
+ }
+
+ // For creating a bordered 'list' of radio, checkbox inputs:
+ &.newspack-ui__input-card {
+ border: 1px solid var(--newspack-ui-color-border);
+ border-radius: var(--newspack-ui-border-radius-m);
+ cursor: pointer;
+ font-weight: normal;
+ gap: 0 var(--newspack-ui-spacer-3);
+ margin-bottom: var(--newspack-ui-spacer-5);
+ padding: var(--newspack-ui-spacer-3);
+ transition: background-color 125ms ease-in-out, border-color 125ms ease-in-out;
+
+ &:has(> input:checked) {
+ background: var(--newspack-ui-color-neutral-5);
+ border-color: var(--newspack-ui-color-neutral-90);
+ }
+
+ + .newspack-ui__input-card {
+ margin-top: calc(var(--newspack-ui-spacer-2) * -1);
+ }
+ }
+
+ // 'Selected' badge
+ &:has(.newspack-ui__badge) {
+ grid-template-columns: var(--newspack-ui-spacer-4) 1fr min-content;
+ }
+
+ .newspack-ui__badge {
+ align-self: start;
+ grid-column-start: 3;
+ grid-row-start: 1;
+ }
+ }
+ }
+
+ &__label-optional {
+ color: var(--newspack-ui-color-neutral-60);
+ font-weight: 400;
+ }
+
+ &__required {
+ color: var(--newspack-ui-color-error-50);
+
+ &[title] {
+ text-decoration: none;
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/forms/_select.scss b/src/newspack-ui/scss/elements/forms/_select.scss
new file mode 100644
index 0000000000..e8a88048d5
--- /dev/null
+++ b/src/newspack-ui/scss/elements/forms/_select.scss
@@ -0,0 +1,20 @@
+.newspack-ui {
+ select {
+ font-family: var(--newspack-ui-font-family);
+ line-height: var(--newspack-ui-line-height-l);
+ }
+
+ .select2-container--default .select2-results__option {
+ &[aria-selected="true"],
+ &[data-selected="true"] {
+ background: var(--newspack-ui-color-neutral-30);
+ }
+
+ &--highlighted {
+ &[aria-selected],
+ &[data-selected] {
+ background: var(--newspack-ui-color-neutral-90);
+ }
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/forms/_spinner.scss b/src/newspack-ui/scss/elements/forms/_spinner.scss
new file mode 100644
index 0000000000..4705356f50
--- /dev/null
+++ b/src/newspack-ui/scss/elements/forms/_spinner.scss
@@ -0,0 +1,35 @@
+.newspack-ui {
+ &__spinner {
+ align-items: center;
+ background: var(--newspack-ui-color-neutral-0);
+ border-radius: var(--newspack-ui-border-radius-m);
+ display: flex;
+ height: 100%;
+ justify-content: center;
+ left: 50%;
+ opacity: 0.7;
+ position: absolute;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 100%;
+ > span {
+ animation: spin 900ms infinite linear;
+ border: 1.5px solid;
+ border-color:
+ var(--newspack-ui-color-neutral-90) var(--newspack-ui-color-neutral-90)
+ var(--newspack-ui-color-body-bg) var(--newspack-ui-color-body-bg);
+ border-radius: 50%;
+ height: calc(var(--newspack-ui-spacer-base) * 2.25);
+ width: calc(var(--newspack-ui-spacer-base) * 2.25);
+ }
+ }
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/newspack-ui/scss/elements/forms/_text-inputs.scss b/src/newspack-ui/scss/elements/forms/_text-inputs.scss
new file mode 100644
index 0000000000..ee47f8398a
--- /dev/null
+++ b/src/newspack-ui/scss/elements/forms/_text-inputs.scss
@@ -0,0 +1,44 @@
+.newspack-ui {
+ // TODO: define some kind of size hierachy for buttons eg. small, medium, large;
+ // not needed for RAS, but likely for My Account.
+ input[type="text"],
+ input[type="email"],
+ input[type="url"],
+ input[type="password"],
+ input[type="search"],
+ input[type="number"],
+ input[type="tel"],
+ input[type="range"],
+ input[type="date"],
+ input[type="month"],
+ input[type="week"],
+ input[type="time"],
+ input[type="datetime"],
+ input[type="datetime-local"],
+ input[type="zip"],
+ input[type="color"],
+ textarea {
+ border-color: var(--newspack-ui-color-input-border);
+ border-radius: var(--newspack-ui-border-radius-m);
+ display: block;
+ font-family: var(--newspack-ui-font-family);
+ font-size: var(--newspack-ui-font-size-s);
+ line-height: var(--newspack-ui-line-height-l);
+ padding: calc(var(--newspack-ui-spacer-2) - 1px);
+ transition:
+ background-color 125ms ease-in-out,
+ border-color 125ms ease-in-out,
+ outline 125ms ease-in-out;
+ width: 100%;
+
+ &:focus-visible {
+ outline: 2px solid var(--newspack-ui-color-input-border-focus);
+ outline-offset: -1px;
+ }
+
+ &:disabled {
+ background-color: var(--newspack-ui-color-input-background-disabled);
+ border-color: var(--newspack-ui-color-input-border-disabled);
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/misc/_badge.scss b/src/newspack-ui/scss/elements/misc/_badge.scss
new file mode 100644
index 0000000000..f533d3ad73
--- /dev/null
+++ b/src/newspack-ui/scss/elements/misc/_badge.scss
@@ -0,0 +1,40 @@
+.newspack-ui__badge {
+ background: var(--newspack-ui-color-neutral-0);
+ border-radius: var(--newspack-ui-border-radius-2xs);
+ color: var(--newspack-ui-color-neutral-90);
+ display: inline-grid;
+ font-size: var(--newspack-ui-font-size-xs);
+ font-weight: 600;
+ line-height: var(--newspack-ui-line-height-xs);
+ padding: 2px 6px; // TODO: replace with variables; they only go down to 8px, perhaps they need to be rethought?
+ place-items: center;
+ text-transform: uppercase;
+
+ &--primary {
+ background: var(--newspack-ui-color-neutral-90);
+ color: var(--newspack-ui-color-neutral-0);
+ }
+
+ &--secondary {
+ background: var(--newspack-ui-color-neutral-10);
+ }
+
+ &--outline {
+ background: var(--newspack-ui-color-neutral-0);
+ color: var(--newspack-ui-color-neutral-60);
+ outline: 1px solid var(--newspack-ui-color-neutral-30);
+ outline-offset: -1px;
+ }
+
+ &--success {
+ background: var(--newspack-ui-color-success-0);
+ }
+
+ &--error {
+ background: var(--newspack-ui-color-error-0);
+ }
+
+ &--warning {
+ background: var(--newspack-ui-color-warning-0);
+ }
+}
diff --git a/src/newspack-ui/scss/elements/misc/_index.scss b/src/newspack-ui/scss/elements/misc/_index.scss
new file mode 100644
index 0000000000..43e06d3caa
--- /dev/null
+++ b/src/newspack-ui/scss/elements/misc/_index.scss
@@ -0,0 +1,3 @@
+@use "badge";
+@use "word-divider";
+@use "reader-auth";
diff --git a/src/newspack-ui/scss/elements/misc/_reader-auth.scss b/src/newspack-ui/scss/elements/misc/_reader-auth.scss
new file mode 100644
index 0000000000..b457f0f4dc
--- /dev/null
+++ b/src/newspack-ui/scss/elements/misc/_reader-auth.scss
@@ -0,0 +1,8 @@
+// Re-adds some modal-like spacing for when the sign up form ends up embedded in a page.
+.newspack-ui {
+ &.newspack-reader-auth {
+ .newspack-ui__button:not(:first-child, a.newspack-ui__button):first-of-type {
+ margin-top: var(--newspack-ui-spacer-5);
+ }
+ }
+}
diff --git a/src/newspack-ui/scss/elements/misc/_word-divider.scss b/src/newspack-ui/scss/elements/misc/_word-divider.scss
new file mode 100644
index 0000000000..92bced0806
--- /dev/null
+++ b/src/newspack-ui/scss/elements/misc/_word-divider.scss
@@ -0,0 +1,16 @@
+.newspack-ui__word-divider {
+ align-items: center;
+ display: flex;
+ font-size: var(--newspack-ui-font-size-xs);
+ gap: var(--newspack-ui-spacer-2);
+ justify-content: space-evenly;
+ margin: var(--newspack-ui-spacer-5) 0;
+
+ &::before,
+ &::after {
+ border-top: 1px solid var(--newspack-ui-color-border);
+ content: "";
+ display: block;
+ width: 100%;
+ }
+}
diff --git a/src/newspack-ui/scss/elements/woocommerce/_index.scss b/src/newspack-ui/scss/elements/woocommerce/_index.scss
new file mode 100644
index 0000000000..f33ef3f432
--- /dev/null
+++ b/src/newspack-ui/scss/elements/woocommerce/_index.scss
@@ -0,0 +1 @@
+@use "overrides";
diff --git a/src/newspack-ui/scss/elements/woocommerce/_overrides.scss b/src/newspack-ui/scss/elements/woocommerce/_overrides.scss
new file mode 100644
index 0000000000..126c0451cf
--- /dev/null
+++ b/src/newspack-ui/scss/elements/woocommerce/_overrides.scss
@@ -0,0 +1,39 @@
+// Some general overrides for WooCommerce styles.
+.newspack-ui {
+ label .optional {
+ color: var(--newspack-ui-color-neutral-60);
+ font-weight: 400;
+ }
+
+ &__modal--small {
+ form p {
+ margin: 0 0 var(--newspack-ui-spacer-2);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .newspack-ui__input-card {
+ label {
+ display: block;
+ }
+ }
+}
+
+// See #3292.
+.woocommerce-account {
+ // Inline sign up form max-width.
+ .woocommerce-notices-wrapper:has(+ .newspack-reader-auth__inline-wrapper),
+ .newspack-reader-auth__inline-wrapper {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: var(--newspack-ui-modal-width-s);
+ }
+
+ .newspack-reader-auth__inline-wrapper h2 {
+ font-size: var(--newspack-ui-font-size-m);
+ font-weight: 600;
+ }
+}
diff --git a/src/newspack-ui/scss/variables/_breakpoints.scss b/src/newspack-ui/scss/variables/_breakpoints.scss
new file mode 100644
index 0000000000..5f07d4d0d2
--- /dev/null
+++ b/src/newspack-ui/scss/variables/_breakpoints.scss
@@ -0,0 +1,3 @@
+$mobile_width: 600px;
+$tablet_width: 782px;
+$desktop_width: 1168px;
diff --git a/src/newspack-ui/scss/variables/_colors.scss b/src/newspack-ui/scss/variables/_colors.scss
new file mode 100644
index 0000000000..7d94487f21
--- /dev/null
+++ b/src/newspack-ui/scss/variables/_colors.scss
@@ -0,0 +1,85 @@
+:root {
+ // Neutral:
+ --newspack-ui-color-neutral-0: #fff;
+ --newspack-ui-color-neutral-5: #f7f7f7;
+ --newspack-ui-color-neutral-10: #f0f0f0;
+ --newspack-ui-color-neutral-20: #e0e0e0;
+ --newspack-ui-color-neutral-30: #ddd;
+ --newspack-ui-color-neutral-40: #ccc;
+ --newspack-ui-color-neutral-50: #949494;
+ --newspack-ui-color-neutral-60: #6c6c6c;
+ --newspack-ui-color-neutral-70: #00000070;
+ --newspack-ui-color-neutral-80: #3e3e3e;
+ --newspack-ui-color-neutral-90: #1e1e1e;
+ --newspack-ui-color-neutral-100: #000;
+
+ // Primary:
+ --newspack-ui-color-primary-0: #f5fdff;
+ --newspack-ui-color-primary-5: #d6f6ff;
+ --newspack-ui-color-primary-10: #b4e4ff;
+ --newspack-ui-color-primary-20: #93ccfd;
+ --newspack-ui-color-primary-30: #72affb;
+ --newspack-ui-color-primary-40: #528dfc;
+ --newspack-ui-color-primary-50: #36f;
+ --newspack-ui-color-primary-60: #2240d5;
+ --newspack-ui-color-primary-70: #1522af;
+ --newspack-ui-color-primary-80: #0b0b8d;
+ --newspack-ui-color-primary-90: #0d046e;
+ --newspack-ui-color-primary-100: #0e0052;
+
+ // Secondary:
+ --newspack-ui-color-secondary-30: #fffff0;
+ --newspack-ui-color-secondary-40: #ffffd3;
+ --newspack-ui-color-secondary-50: #ff0;
+ --newspack-ui-color-secondary-60: #f2f200;
+ --newspack-ui-color-secondary-70: #e4e400;
+
+ // Success:
+ --newspack-ui-color-success-0: #edfaef;
+ --newspack-ui-color-success-5: #b8e6bf;
+ --newspack-ui-color-success-50: #008a20;
+ --newspack-ui-color-success-60: #007017;
+
+ // Error:
+ --newspack-ui-color-error-0: #fcf0f1;
+ --newspack-ui-color-error-5: #facfd2;
+ --newspack-ui-color-error-50: #d63638;
+ --newspack-ui-color-error-60: #b32d2e;
+
+ //Warning:
+ --newspack-ui-color-warning-0: #fcf9e8;
+ --newspack-ui-color-warning-5: #f5e6ab;
+ --newspack-ui-color-warning-30: #dba617;
+ --newspack-ui-color-warning-40: #bd8600;
+
+ // Theme variables - from the classic theme; overridden in class-newspack-ui.php for block theme.
+ // TODO: commented out for now with the assumption all should be blue when used.
+ // --newspack-ui-color-primary: var(--newspack-theme-color-primary, var(--newspack-ui-color-neutral-90));
+ // --newspack-ui-color-against-primary: var(--newspack-theme-color-against-primary, var(--newspack-ui-color-neutral-0));
+ --newspack-ui-color-primary: var(--newspack-ui-color-primary-60);
+ --newspack-ui-color-against-primary: var(--newspack-ui-color-neutral-0);
+
+ // Specific assignments - general:
+ --newspack-ui-color-border: var(--newspack-ui-color-neutral-30);
+ --newspack-ui-color-body-bg: var(--newspack-ui-color-neutral-0);
+
+ // Specific assignments - form elements - buttons:
+ --newspack-ui-color-button-bg: var(--newspack-ui-color-neutral-90);
+ --newspack-ui-color-button-bg-hover: var(--newspack-ui-color-neutral-60);
+ --newspack-ui-color-button-text: var(--newspack-ui-color-neutral-0);
+ --newspack-ui-color-button-text-hover: var(--newspack-ui-color-neutral-0);
+
+ // Specific assignments - form elements - inputs:
+ --newspack-ui-color-input-border: var(--newspack-ui-color-neutral-40);
+ --newspack-ui-color-input-border-focus: var(--newspack-ui-color-neutral-90);
+ --newspack-ui-color-input-border-disabled: var(--newspack-ui-color-neutral-40);
+ --newspack-ui-color-input-background-disabled: var(--newspack-ui-color-neutral-10);
+ --newspack-ui-label-color: var(--newspack-ui-color-neutral-90);
+
+ // Alternatives for when prefers-contrast is set to 'more':
+ @media (prefers-contrast: more) {
+ --newspack-ui-color-input-border: var(--newspack-ui-color-neutral-60);
+ --newspack-ui-color-input-border-disabled: var(--newspack-ui-color-neutral-50);
+ --newspack-ui-color-input-background-disabled: var(--newspack-ui-color-neutral-30);
+ }
+}
diff --git a/src/newspack-ui/scss/variables/_fonts.scss b/src/newspack-ui/scss/variables/_fonts.scss
new file mode 100644
index 0000000000..d094381cee
--- /dev/null
+++ b/src/newspack-ui/scss/variables/_fonts.scss
@@ -0,0 +1,47 @@
+@font-face {
+ font-family: Inter;
+ font-style: normal;
+ font-weight: 100 900;
+ font-display: swap;
+ src: url("./fonts/InterVariable.woff2?v=4.0") format("woff2");
+}
+@font-face {
+ font-family: Inter;
+ font-style: italic;
+ font-weight: 100 900;
+ font-display: swap;
+ src: url("./fonts/InterVariable-Italic.woff2?v=4.0") format("woff2");
+}
+
+:root {
+ // Fonts - general
+ --newspack-ui-font-family: "Inter", system-ui, sans-serif;
+ --newspack-ui-font-weight-strong: 600;
+
+ // Fonts - sizes
+ --newspack-ui-font-size-2xs: 12px;
+ --newspack-ui-font-size-xs: 14px;
+ --newspack-ui-font-size-s: 16px;
+ --newspack-ui-font-size-m: clamp(1.125rem, 0.929rem + 0.402vw, 1.25rem); // 18px - 20px
+ --newspack-ui-font-size-l: clamp(1.25rem, 0.857rem + 0.803vw, 1.5rem); // 20px - 24px
+ --newspack-ui-font-size-xl: clamp(1.375rem, 0.394rem + 2.008vw, 2rem); // 22px - 32px
+ --newspack-ui-font-size-2xl: clamp(1.625rem, 0.055rem + 3.213vw, 2.625rem); // 26px - 42px
+ --newspack-ui-font-size-3xl: clamp(1.75rem, -0.213rem + 4.016vw, 3rem); // 28px - 48px
+ --newspack-ui-font-size-4xl: clamp(2rem, -1.141rem + 6.426vw, 4rem); // 32px - 64px
+ --newspack-ui-font-size-5xl: clamp(2.125rem, -2.39rem + 9.237vw, 5rem); // 34px - 80px
+ --newspack-ui-font-size-6xl: clamp(2.25rem, -3.639rem + 12.048vw, 6rem); // 36px - 96px
+
+ // Fonts - line heights
+ // TODO: would this make more sense as mixins with the accompanying font sizes?
+ --newspack-ui-line-height-2xs: 1.3333;
+ --newspack-ui-line-height-xs: 1.4286;
+ --newspack-ui-line-height-s: 1.5;
+ --newspack-ui-line-height-m: 1.6;
+ --newspack-ui-line-height-l: 1.5;
+ --newspack-ui-line-height-xl: 1.375;
+ --newspack-ui-line-height-2xl: 1.3333;
+ --newspack-ui-line-height-3xl: 1.25;
+ --newspack-ui-line-height-4xl: 1.125;
+ --newspack-ui-line-height-5xl: 1.1;
+ --newspack-ui-line-height-6xl: 1.0833;
+}
diff --git a/src/newspack-ui/scss/variables/_index.scss b/src/newspack-ui/scss/variables/_index.scss
new file mode 100644
index 0000000000..b96f791df3
--- /dev/null
+++ b/src/newspack-ui/scss/variables/_index.scss
@@ -0,0 +1,18 @@
+@use "breakpoints";
+@use "colors";
+@use "fonts";
+@use "spacing";
+
+// TODO: Some misc styles that need a home!
+:root {
+ // Modal widths
+ --newspack-ui-modal-width-l: 964px;
+ --newspack-ui-modal-width-m: 632px;
+ --newspack-ui-modal-width-s: 410px;
+
+ // Border radius - not ems because the font size of the element itself caused a cascade.
+ --newspack-ui-border-radius-m: 6px;
+ --newspack-ui-border-radius-s: 4px;
+ --newspack-ui-border-radius-xs: 3px;
+ --newspack-ui-border-radius-2xs: 2px;
+}
diff --git a/src/newspack-ui/scss/variables/_spacing.scss b/src/newspack-ui/scss/variables/_spacing.scss
new file mode 100644
index 0000000000..93908b62d6
--- /dev/null
+++ b/src/newspack-ui/scss/variables/_spacing.scss
@@ -0,0 +1,13 @@
+:root {
+ // Spacing - not ems because the font size of the element itself caused a cascade
+ --newspack-ui-spacer-base: 8px;
+ --newspack-ui-spacer-2: calc(var(--newspack-ui-spacer-base) * 1.5); // 12px
+ --newspack-ui-spacer-3: calc(var(--newspack-ui-spacer-base) * 2); // 16px
+ --newspack-ui-spacer-4: calc(var(--newspack-ui-spacer-base) * 2.5); // 20px
+ --newspack-ui-spacer-5: calc(var(--newspack-ui-spacer-base) * 3); // 24px
+ --newspack-ui-spacer-6: calc(var(--newspack-ui-spacer-base) * 4); // 32px - TODO: clamp?
+ --newspack-ui-spacer-7: calc(var(--newspack-ui-spacer-base) * 4.5); // 36px - TODO: clamp?
+ --newspack-ui-spacer-8: calc(var(--newspack-ui-spacer-base) * 5); // 40px - TODO: clamp?
+ --newspack-ui-spacer-9: calc(var(--newspack-ui-spacer-base) * 6); // 48px - TODO: clamp?
+ --newspack-ui-spacer-10: calc(var(--newspack-ui-spacer-base) * 8); // 64px - TODO: clamp?
+}
diff --git a/src/newspack-ui/style.scss b/src/newspack-ui/style.scss
new file mode 100644
index 0000000000..d3fbaef50f
--- /dev/null
+++ b/src/newspack-ui/style.scss
@@ -0,0 +1,24 @@
+@use "scss/variables";
+@use "scss/elements";
+@use "scss/modals";
+
+.newspack-ui {
+ // Need better spot for this
+ &__color-text-gray {
+ color: var(--newspack-ui-color-neutral-60);
+ }
+
+ // Overrides for theme styles -- to move to theme?
+ h2:not([class*="font-size"]),
+ h3:not([class*="font-size"]) {
+ font-size: var(--newspack-ui-font-size-s);
+ line-height: var(--newspack-ui-line-height-s);
+ }
+
+ a,
+ a:hover {
+ &:not(.newspack-ui__button) {
+ color: inherit;
+ }
+ }
+}
diff --git a/src/other-scripts/emails/style.scss b/src/other-scripts/emails/style.scss
index 6db5580f8b..a51ef57f84 100644
--- a/src/other-scripts/emails/style.scss
+++ b/src/other-scripts/emails/style.scss
@@ -1,3 +1,16 @@
.editor-post-title {
display: none;
}
+
+// See #3326.
+.editor-styles-wrapper .block-editor-block-list__layout {
+ :not(code) {
+ font-family: Arial, Helvetica, sans-serif !important;
+ }
+
+ .wp-block-button {
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ padding: 0 !important;
+ }
+}
diff --git a/src/other-scripts/recaptcha/index.js b/src/other-scripts/recaptcha/index.js
index 8328335ea5..037228d729 100644
--- a/src/other-scripts/recaptcha/index.js
+++ b/src/other-scripts/recaptcha/index.js
@@ -1,17 +1,7 @@
-/* globals jQuery, grecaptcha, newspack_recaptcha_data, newspack_grecaptcha */
+/* globals jQuery, grecaptcha, newspack_recaptcha_data */
import './style.scss';
-window.newspack_grecaptcha = window.newspack_grecaptcha || {
- widgets: {},
- getCaptchaV3Token,
-};
-
-const isV2 = 'v2' === newspack_recaptcha_data.version.substring( 0, 2 );
-const isV3 = 'v3' === newspack_recaptcha_data.version;
-const siteKey = newspack_recaptcha_data.site_key;
-const isInvisible = 'v2_invisible' === newspack_recaptcha_data.version;
-
/**
* Specify a function to execute when the DOM is fully loaded.
*
@@ -34,89 +24,214 @@ function domReady( callback ) {
document.addEventListener( 'DOMContentLoaded', callback );
}
+window.newspack_grecaptcha = window.newspack_grecaptcha || {
+ destroy,
+ render,
+ version: newspack_recaptcha_data.version,
+};
+
+const isV2 = 'v2' === newspack_recaptcha_data.version.substring( 0, 2 );
+const isV3 = 'v3' === newspack_recaptcha_data.version;
+const siteKey = newspack_recaptcha_data.site_key;
+const isInvisible = 'v2_invisible' === newspack_recaptcha_data.version;
+
/**
- * We need to chain these callbacks to avoid two potential race conditions.
+ * Refresh the reCAPTCHA v3 token for the given form and action.
+ *
+ * @param {HTMLElement} field The hidden input field storing the token for a form.
+ * @param {string} action The action name to pass to reCAPTCHA.
+ *
+ * @return {Promise
+
+
+
+ + |