diff --git a/projects/packages/newsletter/changelog/add-subscribe-button-only-local-popup b/projects/packages/newsletter/changelog/add-subscribe-button-only-local-popup new file mode 100644 index 000000000000..08f70224d4bc --- /dev/null +++ b/projects/packages/newsletter/changelog/add-subscribe-button-only-local-popup @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Newsletter Settings: replace the "Subscribe modal heading" text field with a "Subscribe button pop-up" entry in the same placements grid as the other subscribe pop-ups, editable directly in the Site Editor. diff --git a/projects/packages/newsletter/src/settings/newsletter-settings.tsx b/projects/packages/newsletter/src/settings/newsletter-settings.tsx index e80272d91e9a..775ebd312d75 100644 --- a/projects/packages/newsletter/src/settings/newsletter-settings.tsx +++ b/projects/packages/newsletter/src/settings/newsletter-settings.tsx @@ -30,7 +30,6 @@ import { NewsletterCategoriesSection, NewsletterSection, PaidNewsletterSection, - SubscribeModalSection, SubscriptionsSection, WelcomeEmailSection, } from './sections'; @@ -179,12 +178,6 @@ export function NewsletterSettingsBody( { ); const [ isSavingWelcomeEmail, setIsSavingWelcomeEmail ] = useState( false ); - // Subscribe modal heading state (for manual save). - const [ subscribeModalChanges, setSubscribeModalChanges ] = useState< - Partial< NewsletterSettings > - >( {} ); - const [ isSavingSubscribeModal, setIsSavingSubscribeModal ] = useState( false ); - // Get newsletter script data. const newsletterScriptData = useMemo( () => getNewsletterScriptData(), [] ); @@ -479,38 +472,6 @@ export function NewsletterSettingsBody( { } ); }, [ createErrorNotice, createSuccessNotice, welcomeEmailChanges, data ] ); - // Handle subscribe modal heading changes (staged, not auto-saved). - const handleSubscribeModalChange = useCallback( ( updates: Partial< NewsletterSettings > ) => { - setData( prev => ( { ...prev, ...updates } ) ); - setSubscribeModalChanges( prev => ( { ...prev, ...updates } ) ); - }, [] ); - - // Save subscribe modal heading. - const saveSubscribeModal = useCallback( () => { - if ( ! data ) { - return; - } - - setIsSavingSubscribeModal( true ); - - updateSettings( subscribeModalChanges ) - .then( () => { - setSavedData( prev => ( { ...prev, ...subscribeModalChanges } ) ); - setSubscribeModalChanges( {} ); - createSuccessNotice( __( 'Subscribe modal heading saved', 'jetpack-newsletter' ) ); - } ) - .catch( ( err: Error ) => { - // eslint-disable-next-line no-console - console.error( 'Newsletter subscribe modal save error:', err ); - createErrorNotice( - err.message || __( 'Failed to save subscribe modal heading', 'jetpack-newsletter' ) - ); - } ) - .finally( () => { - setIsSavingSubscribeModal( false ); - } ); - }, [ createErrorNotice, createSuccessNotice, subscribeModalChanges, data ] ); - if ( isLoading ) { return (
@@ -537,7 +498,6 @@ export function NewsletterSettingsBody( { const hasSenderNameChanges = Object.keys( senderNameChanges ).length > 0; const hasNewsletterCategoriesChanges = Object.keys( newsletterCategoriesChanges ).length > 0; const hasWelcomeEmailChanges = Object.keys( welcomeEmailChanges ).length > 0; - const hasSubscribeModalChanges = Object.keys( subscribeModalChanges ).length > 0; return ( <> @@ -626,16 +586,6 @@ export function NewsletterSettingsBody( { isNewsletterEnabled={ data.subscriptions } /> - - - - diff --git a/projects/packages/newsletter/src/settings/sections/index.ts b/projects/packages/newsletter/src/settings/sections/index.ts index 6cab19e733f9..740e3c4dc906 100644 --- a/projects/packages/newsletter/src/settings/sections/index.ts +++ b/projects/packages/newsletter/src/settings/sections/index.ts @@ -10,6 +10,5 @@ export { LegacySubscriptionsSection } from './legacy-subscriptions-section'; export { NewsletterCategoriesSection } from './newsletter-categories-section'; export { NewsletterSection } from './newsletter-section'; export { PaidNewsletterSection } from './paid-newsletter-section'; -export { SubscribeModalSection } from './subscribe-modal-section'; export { SubscriptionsSection } from './subscriptions-section'; export { WelcomeEmailSection } from './welcome-email-section'; diff --git a/projects/packages/newsletter/src/settings/sections/subscribe-modal-section.tsx b/projects/packages/newsletter/src/settings/sections/subscribe-modal-section.tsx deleted file mode 100644 index 1492d05b419f..000000000000 --- a/projects/packages/newsletter/src/settings/sections/subscribe-modal-section.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/** - * External dependencies - */ -import analytics from '@automattic/jetpack-analytics'; -import { getSiteType, isWpcomPlatformSite } from '@automattic/jetpack-script-data'; -import { WpcomSupportLink } from '@automattic/jetpack-shared-extension-utils/components/wpcom-support-link'; -import { DataForm, type Field } from '@wordpress/dataviews'; -import { createInterpolateElement, useCallback, useMemo } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { Button, Card, Fieldset, Link, Text } from '@wordpress/ui'; -/** - * Internal dependencies - */ -import type { NewsletterSettings } from '../types'; - -interface SubscribeModalSectionProps { - data: NewsletterSettings; - onChange: ( updates: Partial< NewsletterSettings > ) => void; - onSave: () => void; - isSaving: boolean; - hasChanges: boolean; - /** Setting keys staged in this section's changeset, fed into section_save analytics. */ - changedKeys?: string[]; - isNewsletterEnabled: boolean; -} - -// Flattened data structure for DataForm -interface SubscribeModalFormData { - subscribe_modal_heading: string; -} - -/** - * Subscribe Modal Section Component - * - * Configures the heading shown above the email input in the Subscribe block's modal popup. - * Only takes effect when a Subscribe block uses the "Button only" style. - * - * @param {SubscribeModalSectionProps} props - Component props - * @return {JSX.Element} The subscribe modal section - */ -export function SubscribeModalSection( { - data, - onChange, - onSave, - isSaving, - hasChanges, - changedKeys, - isNewsletterEnabled, -}: SubscribeModalSectionProps ): JSX.Element { - const siteType = getSiteType(); - - const formData: SubscribeModalFormData = useMemo( - () => ( { - subscribe_modal_heading: data.subscription_options?.subscribe_modal_heading || '', - } ), - [ data.subscription_options?.subscribe_modal_heading ] - ); - - const savingText = __( 'Saving…', 'jetpack-newsletter' ); - const saveText = __( 'Save', 'jetpack-newsletter' ); - - const handleSave = useCallback( () => { - analytics.tracks.recordEvent( 'jetpack_newsletter_section_save', { - site_type: siteType, - section: 'subscribe_modal', - changed_keys: ( changedKeys ?? [] ).join( ',' ), - change_count: ( changedKeys ?? [] ).length, - } ); - onSave(); - }, [ changedKeys, onSave, siteType ] ); - - const isWpcom = isWpcomPlatformSite(); - const buttonOnlyStyleUrl = isWpcom - ? 'https://wordpress.com/support/wordpress-editor/blocks/subscribe-block/#change-the-subscription-box-appearance' - : 'https://jetpack.com/support/jetpack-blocks/subscription-form-block/#use-the-button-only-style'; - - const ButtonOnlyStyleLink = isWpcom ? ( - - ) : ( - - ); - - const fields: Field< SubscribeModalFormData >[] = [ - { - id: 'subscribe_modal_heading', - label: __( 'Subscribe modal heading', 'jetpack-newsletter' ), - type: 'text' as const, - Edit: 'textarea' as const, - placeholder: __( 'Subscribe now to stay ahead and never miss a beat!', 'jetpack-newsletter' ), - description: createInterpolateElement( - __( - 'Only affects Subscribe blocks using the "Button only" style. Leave blank to use the default heading.', - 'jetpack-newsletter' - ), - { - link: ButtonOnlyStyleLink, - } - ), - }, - ]; - - const handleDataFormChange = useCallback( - ( updates: Partial< SubscribeModalFormData > ) => { - if ( updates.subscribe_modal_heading !== undefined ) { - // Preserve all properties of subscription_options when updating - onChange( { - subscription_options: { - invitation: data.subscription_options?.invitation || '', - welcome: data.subscription_options?.welcome || '', - comment_follow: data.subscription_options?.comment_follow || '', - subscribe_modal_heading: updates.subscribe_modal_heading, - }, - } ); - } - }, - [ onChange, data.subscription_options ] - ); - - return ( - - - { __( 'Subscribe modal heading', 'jetpack-newsletter' ) } - - -

- - { __( - 'Shown at the top of the subscribe popup that appears when a visitor clicks a Subscribe block.', - 'jetpack-newsletter' - ) } - -

- - - -
- -
-
-
- ); -} diff --git a/projects/packages/newsletter/src/settings/sections/subscriptions-section.tsx b/projects/packages/newsletter/src/settings/sections/subscriptions-section.tsx index d3e2e0b08897..5cae3aa89e68 100644 --- a/projects/packages/newsletter/src/settings/sections/subscriptions-section.tsx +++ b/projects/packages/newsletter/src/settings/sections/subscriptions-section.tsx @@ -36,6 +36,10 @@ const PLACEMENT_SLUG_BY_KEY: Record< string, string > = { jetpack_subscribe_floating_button_enabled: 'floating_button', }; +// The "Subscribe button pop-up" card below has no underlying setting to +// toggle (see its usage), so its `onChange` is inert by design. +const noop = () => {}; + interface SubscriptionsSectionProps { data: NewsletterSettings; /** @@ -263,6 +267,31 @@ export function SubscriptionsSection( { /> ); } ) } + { /* Not a real placement toggle — the pop-up shown when a + visitor clicks a "Button only" style Subscribe block is + inherent to using that block style, with no separate + enable/disable setting. Always checked and disabled to + reflect that, with the same "Preview and edit" link as + its siblings. */ } + } + previewUrl={ + canShowBlockThemeEditorLinks + ? addQueryArgs( getAdminUrl( 'site-editor.php' ), { + postType: 'wp_template_part', + postId: `${ newsletterScriptData.themeStylesheet }//jetpack-subscribe-modal-button`, + canvas: 'edit', + } ) + : undefined + } + checked + disabled + onChange={ noop } + onPreviewClick={ handlePlacementPreviewClick } + />
diff --git a/projects/plugins/jetpack/changelog/add-subscribe-button-only-local-popup b/projects/plugins/jetpack/changelog/add-subscribe-button-only-local-popup new file mode 100644 index 000000000000..f9077909c9ec --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-subscribe-button-only-local-popup @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Subscribe Block: show a local, editable pop-up for the "Button only" style before opening the subscribe checkout, instead of opening checkout with no message to show. The floating subscribe button, which uses the same style internally, benefits from this too. diff --git a/projects/plugins/jetpack/extensions/blocks/subscriptions/view.js b/projects/plugins/jetpack/extensions/blocks/subscriptions/view.js index fa8a273f5708..cc82687bfd00 100644 --- a/projects/plugins/jetpack/extensions/blocks/subscriptions/view.js +++ b/projects/plugins/jetpack/extensions/blocks/subscriptions/view.js @@ -78,6 +78,28 @@ domReady( function () { if ( action === 'subscribe' ) { event.preventDefault(); + // "Button only" style renders no visible email input, so email + // is empty here. Rather than opening the checkout iframe with no + // email (which then has to ask for it inside the iframe itself), + // open a local pop-up with its own embedded Subscribe block first. + // That pop-up's own form submit re-enters this same handler with + // email populated and is-style-button absent, and flows through + // normally below. + const isButtonOnlyStyle = form + .closest( '.wp-block-jetpack-subscriptions' ) + ?.classList.contains( 'is-style-button' ); + if ( ! email && isButtonOnlyStyle ) { + const modal = document.querySelector( '.jetpack-subscribe-modal-button' ); + if ( modal ) { + modal.classList.add( 'open' ); + document.body.classList.add( 'jetpack-subscribe-modal-button-open' ); + modal.querySelector( 'input[type="email"]' )?.focus(); + } + button.classList.remove( 'is-loading' ); + button.setAttribute( 'aria-busy', 'false' ); + return; + } + const post_id = form.querySelector( 'input[name=post_id]' )?.value ?? ''; const tier_id = form.querySelector( 'input[name=tier_id]' )?.value ?? ''; const app_source = form.querySelector( 'input[name=app_source]' )?.value ?? ''; diff --git a/projects/plugins/jetpack/modules/subscriptions.php b/projects/plugins/jetpack/modules/subscriptions.php index 557d2f08ccf0..145f9cb343d8 100644 --- a/projects/plugins/jetpack/modules/subscriptions.php +++ b/projects/plugins/jetpack/modules/subscriptions.php @@ -1146,6 +1146,7 @@ public function track_newsletter_category_creation() { require __DIR__ . '/subscriptions/subscribe-modal/class-jetpack-subscribe-modal.php'; require __DIR__ . '/subscriptions/subscribe-overlay/class-jetpack-subscribe-overlay.php'; require __DIR__ . '/subscriptions/subscribe-floating-button/class-jetpack-subscribe-floating-button.php'; +require __DIR__ . '/subscriptions/subscribe-modal-button/class-jetpack-subscribe-modal-button.php'; require __DIR__ . '/subscriptions/newsletter-widget/class-jetpack-newsletter-dashboard-widget.php'; require_once __DIR__ . '/subscriptions/abilities/class-newsletter-abilities.php'; diff --git a/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/class-jetpack-subscribe-modal-button.php b/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/class-jetpack-subscribe-modal-button.php new file mode 100644 index 000000000000..80f8b0503f20 --- /dev/null +++ b/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/class-jetpack-subscribe-modal-button.php @@ -0,0 +1,168 @@ + +
+
+ +
+
+ get_template(); + } + } + + return $block_template; + } + + /** + * Returns a custom template for the pop-up. + * + * @return WP_Block_Template + */ + public function get_template() { + $template = new WP_Block_Template(); + $template->theme = get_stylesheet(); + $template->slug = self::BLOCK_TEMPLATE_PART_SLUG; + $template->id = self::get_block_template_part_id(); + $template->area = 'uncategorized'; + $template->content = $this->get_subscribe_modal_button_template_content(); + $template->source = 'plugin'; + $template->type = 'wp_template_part'; + $template->title = __( 'Jetpack Subscribe button modal', 'jetpack' ); + $template->status = 'publish'; + $template->has_theme_file = false; + $template->is_custom = true; + $template->description = __( 'The pop-up shown when a visitor clicks a "Button only" style Subscribe block.', 'jetpack' ); + + return $template; + } + + /** + * Returns the initial content of the pop-up template. + * This can then be edited by the user. + * + * Seeds the heading from the legacy `subscribe_modal_heading` site + * setting when present, so a site that already customized it via the + * Newsletter settings page doesn't silently lose that text the first + * time this ships. Once the owner edits this template part directly, the + * template part becomes the sole source of truth for the heading. + * + * @return string + */ + public function get_subscribe_modal_button_template_content() { + $subscription_options = (array) get_option( 'subscription_options', array() ); + $legacy_heading = isset( $subscription_options['subscribe_modal_heading'] ) + ? trim( (string) $subscription_options['subscribe_modal_heading'] ) + : ''; + $heading_text = '' !== $legacy_heading + ? $legacy_heading + : __( 'Subscribe now to stay ahead and never miss a beat!', 'jetpack' ); + + $group_block_name = esc_attr__( 'Subscribe button pop-up container', 'jetpack' ); + + return << +
+ + +

$heading_text

+ + + +
+ +HTML; + } +} + +Jetpack_Subscribe_Modal_Button::init(); + +add_action( + 'rest_api_switched_to_blog', + function () { + Jetpack_Subscribe_Modal_Button::init(); + } +); diff --git a/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/subscribe-modal-button.css b/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/subscribe-modal-button.css new file mode 100644 index 000000000000..8b84462d8d32 --- /dev/null +++ b/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/subscribe-modal-button.css @@ -0,0 +1,58 @@ +body.jetpack-subscribe-modal-button-open { + overflow: hidden; +} + +.jetpack-subscribe-modal-button { + visibility: hidden; + position: fixed; + z-index: 50000; /* Same as WP.com Action bar */ + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: transparent; + transition: all 0.4s; +} + +.jetpack-subscribe-modal-button.open { + background-color: rgba(0, 0, 0, 0.3); + visibility: visible; +} + +.jetpack-subscribe-modal-button__modal-content { + position: relative; + visibility: hidden; + overflow: hidden; + top: 100%; + background-color: #fefefe; + margin: 15% auto; + width: 100%; + max-width: 600px; + border-radius: 10px; + box-sizing: border-box; + transition: all 0.4s; + text-wrap: balance; +} + +.jetpack-subscribe-modal-button.open .jetpack-subscribe-modal-button__modal-content { + top: 0; + visibility: visible; +} + +/* + * These text-wrap properties still have limited browser + * support, but based on feedback still adding them for when + * they are supported. + */ +.jetpack-subscribe-modal-button__modal-content p { + text-wrap: balance; + text-wrap: pretty; +} + +@media screen and (max-width: 640px) { + + .jetpack-subscribe-modal-button__modal-content { + width: 94%; + } +} diff --git a/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/subscribe-modal-button.js b/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/subscribe-modal-button.js new file mode 100644 index 000000000000..ebaf6d6f00cf --- /dev/null +++ b/projects/plugins/jetpack/modules/subscriptions/subscribe-modal-button/subscribe-modal-button.js @@ -0,0 +1,35 @@ +const { domReady } = wp; +domReady( () => { + const modal = document.querySelector( '.jetpack-subscribe-modal-button' ); + if ( ! modal ) { + return; + } + + function closeModal() { + modal.classList.remove( 'open' ); + document.body.classList.remove( 'jetpack-subscribe-modal-button-open' ); + } + + // Opening is triggered from the "Button only" Subscribe block's own + // submit handler (extensions/blocks/subscriptions/view.js), not here — + // this file only wires up how to close it again once open. + window.addEventListener( 'click', event => { + if ( event.target === modal ) { + closeModal(); + } + } ); + + window.addEventListener( 'keydown', event => { + if ( event.key === 'Escape' && modal.classList.contains( 'open' ) ) { + closeModal(); + } + } ); + + // When the pop-up's own embedded Subscribe block form is submitted, the + // checkout iframe takes over — hide this pop-up so it's not left open + // underneath it. + const form = modal.querySelector( 'form' ); + if ( form ) { + form.addEventListener( 'subscription-modal-loaded', closeModal ); + } +} );