diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c05d176bea98e..8444dee081798 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3373,6 +3373,18 @@ importers: projects/packages/newsletter: dependencies: + '@automattic/jetpack-api': + specifier: workspace:* + version: link:../../js-packages/api + '@automattic/jetpack-components': + specifier: workspace:* + version: link:../../js-packages/components + '@automattic/jetpack-shared-extension-utils': + specifier: workspace:* + version: link:../../js-packages/shared-extension-utils + '@wordpress/components': + specifier: 30.9.0 + version: 30.9.0(patch_hash=2659f08edd4c0250f15fb428f013852a17e84da9c745e6dae6307de837e4d30b)(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/dataviews': specifier: 11.0.0 version: 11.0.0(patch_hash=2659f08edd4c0250f15fb428f013852a17e84da9c745e6dae6307de837e4d30b)(@types/react@18.3.26)(react@18.3.1) @@ -3382,9 +3394,15 @@ importers: '@wordpress/i18n': specifier: 6.9.0 version: 6.9.0(patch_hash=0c63a888feb97f2f1d416ca013ad85c31b6360b41cc0b6e2b0ae28f778fbdc5b) + '@wordpress/notices': + specifier: 5.36.0 + version: 5.36.0(patch_hash=0c63a888feb97f2f1d416ca013ad85c31b6360b41cc0b6e2b0ae28f778fbdc5b)(react@18.3.1) + '@wordpress/url': + specifier: 4.36.0 + version: 4.36.0(patch_hash=2659f08edd4c0250f15fb428f013852a17e84da9c745e6dae6307de837e4d30b) debug: - specifier: 4.4.1 - version: 4.4.1 + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@automattic/babel-plugin-replace-textdomain': specifier: workspace:* diff --git a/projects/packages/newsletter/changelog/dotcom-15286-build-out-the-settings-screen b/projects/packages/newsletter/changelog/dotcom-15286-build-out-the-settings-screen new file mode 100644 index 0000000000000..13c823a23de41 --- /dev/null +++ b/projects/packages/newsletter/changelog/dotcom-15286-build-out-the-settings-screen @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Implement comprehensive newsletter settings UI with DataForm-based interface, toggle controls, and card-based design diff --git a/projects/packages/newsletter/package.json b/projects/packages/newsletter/package.json index e365af33ff6a2..978bd47898b6d 100644 --- a/projects/packages/newsletter/package.json +++ b/projects/packages/newsletter/package.json @@ -32,10 +32,16 @@ "extends @wordpress/browserslist-config" ], "dependencies": { + "@automattic/jetpack-api": "workspace:*", + "@automattic/jetpack-components": "workspace:*", + "@automattic/jetpack-shared-extension-utils": "workspace:*", + "@wordpress/components": "30.9.0", "@wordpress/dataviews": "11.0.0", "@wordpress/element": "6.36.0", "@wordpress/i18n": "6.9.0", - "debug": "4.4.1" + "@wordpress/notices": "5.36.0", + "@wordpress/url": "4.36.0", + "debug": "4.4.3" }, "devDependencies": { "@automattic/babel-plugin-replace-textdomain": "workspace:*", diff --git a/projects/packages/newsletter/src/class-settings.php b/projects/packages/newsletter/src/class-settings.php index b8d11ec5825bc..0d72c56a256cf 100644 --- a/projects/packages/newsletter/src/class-settings.php +++ b/projects/packages/newsletter/src/class-settings.php @@ -9,6 +9,7 @@ use Automattic\Jetpack\Admin_UI\Admin_Menu; use Automattic\Jetpack\Assets; +use Automattic\Jetpack\Modules; use Automattic\Jetpack\Paths; use Automattic\Jetpack\Status\Host; @@ -51,6 +52,15 @@ private function expose_to_users() { return apply_filters( 'jetpack_wp_admin_newsletter_settings_enabled', false ); } + /** + * Check if the subscriptions module is active. + * + * @return bool + */ + private function is_subscriptions_active() { + return ( new Modules() )->is_active( 'subscriptions' ); + } + /** * Subscribe to necessary hooks. */ @@ -75,25 +85,41 @@ function () { * Add the newsletter settings menu to the Jetpack menu. */ public function add_wp_admin_menu() { - if ( ( new Host() )->is_wpcom_platform() ) { - $page_suffix = add_submenu_page( - 'jetpack', + $is_module_active = $this->is_subscriptions_active(); + $host = new Host(); + + // Determine parent slug and menu registration method. + // - wpcom simple: Always show in Jetpack menu (module always active). + // - wpcom atomic: Show in Jetpack menu if active, hidden page if inactive. + // - Jetpack: Show in Jetpack menu if active, hidden page if inactive. + if ( $host->is_wpcom_platform() ) { + $parent_slug = ( $host->is_wpcom_simple() || $is_module_active ) ? 'jetpack' : ''; + $use_jetpack_menu = false; // Use add_submenu_page for all wpcom sites. + } else { + $parent_slug = $is_module_active ? 'jetpack' : ''; + $use_jetpack_menu = $is_module_active; + } + + // Register menu item. + if ( $use_jetpack_menu ) { + $page_suffix = Admin_Menu::add_menu( /** "Newsletter" is a product name, do not translate. */ 'Newsletter', 'Newsletter', 'manage_options', 'jetpack-newsletter', - array( $this, 'render' ) + array( $this, 'render' ), + 10 ); } else { - $page_suffix = Admin_Menu::add_menu( + $page_suffix = add_submenu_page( + $parent_slug, /** "Newsletter" is a product name, do not translate. */ 'Newsletter', 'Newsletter', 'manage_options', 'jetpack-newsletter', - array( $this, 'render' ), - 10 + array( $this, 'render' ) ); } @@ -123,6 +149,53 @@ public function load_admin_scripts() { 'enqueue' => true, ) ); + + wp_add_inline_script( + 'jetpack-newsletter', + 'window.jetpackNewsletterSettings = ' . wp_json_encode( $this->get_settings_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';', + 'before' + ); + } + + /** + * Get the data to be passed to the newsletter settings page. + * + * @return array + */ + private function get_settings_data() { + $current_user = wp_get_current_user(); + $theme = wp_get_theme(); + + $site_url = get_site_url(); + $site_raw_url = preg_replace( '(^https?://)', '', $site_url ); + + $host = new Host(); + $blog_id = (int) $host->get_wpcom_site_id(); + $is_wpcom = $host->is_wpcom_platform(); + $is_wpcom_simple = $host->is_wpcom_simple(); + $base_url = $is_wpcom ? 'https://wordpress.com/earn/payments/' : 'https://cloud.jetpack.com/monetize/payments/'; + $setup_payment_plan_url = $base_url . rawurlencode( $site_raw_url ); + + return array( + 'isBlockTheme' => wp_is_block_theme(), + 'siteAdminUrl' => admin_url(), + 'themeStylesheet' => $theme->get_stylesheet(), + 'blogID' => $blog_id, + 'siteRawUrl' => $site_raw_url, + 'email' => $current_user->user_email, + 'gravatar' => get_avatar_url( $current_user->ID ), + 'displayName' => $current_user->display_name, + 'dateExample' => gmdate( get_option( 'date_format' ), time() ), + 'wpAdminSubscriberManagementEnabled' => apply_filters( 'jetpack_wpcom_subscriber_management_enabled', false ), + 'isSubscriptionSiteEditSupported' => wp_is_block_theme(), + 'setupPaymentPlansUrl' => $setup_payment_plan_url, + 'isSitePublic' => (int) get_option( 'blog_public' ) === 1, + 'isWpcomPlatform' => $is_wpcom, + 'isWpcomSimple' => $is_wpcom_simple, + 'isSubscriptionsActive' => $this->is_subscriptions_active(), + 'restApiRoot' => esc_url_raw( rest_url() ), + 'restApiNonce' => wp_create_nonce( 'wp_rest' ), + ); } /** diff --git a/projects/packages/newsletter/src/settings/components/byline-preview.scss b/projects/packages/newsletter/src/settings/components/byline-preview.scss new file mode 100644 index 0000000000000..629b26b6a7d3e --- /dev/null +++ b/projects/packages/newsletter/src/settings/components/byline-preview.scss @@ -0,0 +1,36 @@ +// Byline preview component +.byline-preview { + display: inline-flex; + align-items: center; + flex-flow: row; + margin: 16px 0; + padding: 16px; + background-color: #f0f0f1; + border-radius: 4px; + + &__label { + height: 24px; + line-height: 24px; + margin-right: 8px; + color: #1e1e1e; + } + + &__gravatar { + border-radius: 50%; + width: 24px; + height: 24px; + margin: 0 8px 0 0; + } + + &__author { + color: #1e1e1e; + } + + &__date { + color: #1e1e1e; + } + + em { + color: #646970; + } +} diff --git a/projects/packages/newsletter/src/settings/components/byline-preview.tsx b/projects/packages/newsletter/src/settings/components/byline-preview.tsx new file mode 100644 index 0000000000000..28231da144452 --- /dev/null +++ b/projects/packages/newsletter/src/settings/components/byline-preview.tsx @@ -0,0 +1,97 @@ +/** + * External dependencies + */ +import { createInterpolateElement } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './byline-preview.scss'; + +interface BylinePreviewProps { + isGravatarEnabled: boolean; + isAuthorEnabled: boolean; + isPostDateEnabled: boolean; + gravatar?: string; + displayName: string; + dateExample: string; +} + +/** + * Byline Preview Component + * + * Shows a preview of how the email byline will appear based on the enabled settings. + * + * @param {BylinePreviewProps} props - Component props + * @return {JSX.Element} The byline preview + */ +export function BylinePreview( { + isGravatarEnabled, + isAuthorEnabled, + isPostDateEnabled, + gravatar, + displayName, + dateExample, +}: BylinePreviewProps ): JSX.Element { + if ( ! isGravatarEnabled && ! isAuthorEnabled && ! isPostDateEnabled ) { + return ( +
+ + { createInterpolateElement( + /* translators: placeholder is set to "Byline will be empty" */ + __( + 'Preview: Byline will be empty', + 'jetpack-newsletter' + ), + { + Preview: , + Empty: , + } + ) } + +
+ ); + } + + let byline: JSX.Element | string = ''; + + if ( isAuthorEnabled && isPostDateEnabled ) { + byline = createInterpolateElement( + sprintf( + /* translators: %1$s placeholder is the user display name, %2$s is example date */ + __( 'By %1$s on %2$s', 'jetpack-newsletter' ), + displayName, + dateExample + ), + { + Author: { displayName }, + Date: { dateExample }, + } + ); + } else if ( isAuthorEnabled && ! isPostDateEnabled ) { + byline = createInterpolateElement( + /* translators: %1$s placeholder is the user display name */ + sprintf( __( 'By %1$s', 'jetpack-newsletter' ), displayName ), + { + Author: , + } + ); + } else if ( ! isAuthorEnabled && isPostDateEnabled ) { + byline = { dateExample }; + } + + return ( +
+ { __( 'Preview:', 'jetpack-newsletter' ) } + { isGravatarEnabled && gravatar && ( + { + ) } + { byline } +
+ ); +} diff --git a/projects/packages/newsletter/src/settings/components/header.scss b/projects/packages/newsletter/src/settings/components/header.scss new file mode 100644 index 0000000000000..8ddaa744ceef2 --- /dev/null +++ b/projects/packages/newsletter/src/settings/components/header.scss @@ -0,0 +1,17 @@ +.newsletter-settings__header { + margin-bottom: 2em; + + &-top { + display: flex; + align-items: center; + gap: 1em; + } +} + +.newsletter-settings__title { + margin: 0; +} + +.newsletter-settings__tagline { + margin: 8px 0 0; +} diff --git a/projects/packages/newsletter/src/settings/components/header.tsx b/projects/packages/newsletter/src/settings/components/header.tsx new file mode 100644 index 0000000000000..ca4587f272e0a --- /dev/null +++ b/projects/packages/newsletter/src/settings/components/header.tsx @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { JetpackLogo } from '@automattic/jetpack-components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './header.scss'; + +/** + * Newsletter Settings Header Component + * + * @return {JSX.Element} The header component. + */ +export function Header(): JSX.Element { + return ( +
+
+ +

+ { __( 'Newsletter Settings', 'jetpack-newsletter' ) } +

+
+

+ { __( + 'Transform your blog posts into newsletters to easily reach your subscribers.', + 'jetpack-newsletter' + ) } +

+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/components/toggle-with-link.scss b/projects/packages/newsletter/src/settings/components/toggle-with-link.scss new file mode 100644 index 0000000000000..b1368ae369854 --- /dev/null +++ b/projects/packages/newsletter/src/settings/components/toggle-with-link.scss @@ -0,0 +1,5 @@ +.toggle-with-link__label { + display: inline-flex; + align-items: center; + gap: 0.5em; +} diff --git a/projects/packages/newsletter/src/settings/components/toggle-with-link.tsx b/projects/packages/newsletter/src/settings/components/toggle-with-link.tsx new file mode 100644 index 0000000000000..46e50db37ceb5 --- /dev/null +++ b/projects/packages/newsletter/src/settings/components/toggle-with-link.tsx @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import { ExternalLink, ToggleControl } from '@wordpress/components'; +import { type Field } from '@wordpress/dataviews/wp'; +import { useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import './toggle-with-link.scss'; + +interface ToggleWithLinkProps { + data: Record< string, unknown >; + field: Field< Record< string, unknown > >; + onChange: ( updates: Record< string, unknown > ) => void; + url: string; + linkText: string; + isExternal?: boolean; +} + +/** + * Generic toggle control with a link in the label + * + * @param {object} props - Component props + * @param {object} props.data - The data object + * @param {object} props.field - The field definition + * @param {Function} props.onChange - Change handler + * @param {string} props.url - URL for the link + * @param {string} props.linkText - Text for the link + * @param {boolean} props.isExternal - Whether the link is external (default: true) + * @return {JSX.Element} The toggle control with link + */ +export function ToggleWithLink( { + data, + field, + onChange, + url, + linkText, + isExternal = true, +}: ToggleWithLinkProps ): JSX.Element { + const handleChange = useCallback( () => { + onChange( { [ field.id ]: ! data[ field.id ] } ); + }, [ data, field.id, onChange ] ); + + return ( + + { field.label } + { isExternal ? ( + { linkText } + ) : ( + { linkText } + ) } + + } + help={ field.description } + /> + ); +} + +interface ToggleWithEditorLinkProps { + data: Record< string, unknown >; + field: Field< Record< string, unknown > >; + onChange: ( updates: Record< string, unknown > ) => void; + siteAdminUrl: string; + themeStylesheet: string; + postType: 'wp_template' | 'wp_template_part'; + templateId: string; +} + +/** + * Toggle control with a "Preview and edit" link to the site editor + * + * @param {object} props - Component props + * @param {object} props.data - The data object + * @param {object} props.field - The field definition + * @param {Function} props.onChange - Change handler + * @param {string} props.siteAdminUrl - Site admin URL + * @param {string} props.themeStylesheet - Theme stylesheet name + * @param {string} props.postType - Post type (wp_template or wp_template_part) + * @param {string} props.templateId - Template ID + * @return {JSX.Element} The toggle control with editor link + */ +export function ToggleWithEditorLink( { + data, + field, + onChange, + siteAdminUrl, + themeStylesheet, + postType, + templateId, +}: ToggleWithEditorLinkProps ): JSX.Element { + const url = addQueryArgs( `${ siteAdminUrl }site-editor.php`, { + postType, + postId: `${ themeStylesheet }//${ templateId }`, + canvas: 'edit', + } ); + + return ( + + ); +} diff --git a/projects/packages/newsletter/src/settings/index.tsx b/projects/packages/newsletter/src/settings/index.tsx index d54aa3e77dcfb..110a6f442c4bd 100644 --- a/projects/packages/newsletter/src/settings/index.tsx +++ b/projects/packages/newsletter/src/settings/index.tsx @@ -1,28 +1,387 @@ /** * External dependencies */ -import { createRoot } from '@wordpress/element'; - +import restApi from '@automattic/jetpack-api'; +import { Notice, Snackbar } from '@wordpress/components'; +import { createRoot, useCallback, useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ +import { Header } from './components/header'; +import { + EmailContentSection, + EmailBylineSection, + EmailSenderSettingsSection, + EmailReplyToSettingsSection, + NewsletterSection, + NewsletterCategoriesSection, + PaidNewsletterSection, + SubscriptionsSection, + WelcomeEmailSection, +} from './sections'; +import type { NewsletterSettings, JetpackNewsletterSettings } from './types'; import './style.scss'; /** * Newsletter Settings App * - * @return {Element} The newsletter settings component. + * @return {JSX.Element | null} The newsletter settings component or null. */ -function NewsletterSettingsApp() { +function NewsletterSettingsApp(): JSX.Element | null { + const [ data, setData ] = useState< NewsletterSettings | null >( null ); + const [ isLoading, setIsLoading ] = useState( true ); + const [ error, setError ] = useState< string | null >( null ); + + // Subscription settings state (for manual save) + const [ subscriptionChanges, setSubscriptionChanges ] = useState< Partial< NewsletterSettings > >( + {} + ); + const [ isSavingSubscriptions, setIsSavingSubscriptions ] = useState( false ); + + // Sender name state (for manual save) + const [ senderNameChanges, setSenderNameChanges ] = useState< Partial< NewsletterSettings > >( + {} + ); + const [ isSavingSenderName, setIsSavingSenderName ] = useState( false ); + + // Snackbar notification state + const [ snackbarMessage, setSnackbarMessage ] = useState< string | null >( null ); + + // Newsletter categories state (for manual save) + const [ newsletterCategoriesChanges, setNewsletterCategoriesChanges ] = useState< + Partial< NewsletterSettings > + >( {} ); + const [ isSavingNewsletterCategories, setIsSavingNewsletterCategories ] = useState( false ); + + // Welcome email state (for manual save) + const [ welcomeEmailChanges, setWelcomeEmailChanges ] = useState< Partial< NewsletterSettings > >( + {} + ); + const [ isSavingWelcomeEmail, setIsSavingWelcomeEmail ] = useState( false ); + + // Get settings from PHP + const jetpackSettings = ( + window as Window & { jetpackNewsletterSettings?: JetpackNewsletterSettings } + ).jetpackNewsletterSettings; + + // Callback to clear snackbar + const clearSnackbar = useCallback( () => setSnackbarMessage( null ), [] ); + + // Load settings on mount + useEffect( () => { + // Initialize the REST API with settings from PHP + if ( jetpackSettings?.restApiRoot && jetpackSettings?.restApiNonce ) { + restApi.setApiRoot( jetpackSettings.restApiRoot ); + restApi.setApiNonce( jetpackSettings.restApiNonce ); + } + + restApi + .fetchSettings() + .then( ( settings: Record< string, unknown > ) => { + // Convert category IDs from numbers to strings + const normalizedSettings: NewsletterSettings = { + ...( settings as NewsletterSettings ), + wpcom_newsletter_categories: ( + ( settings.wpcom_newsletter_categories as number[] ) || [] + ).map( String ), + }; + setData( normalizedSettings ); + setIsLoading( false ); + } ) + .catch( ( err: Error ) => { + setError( err.message || 'Failed to load settings' ); + setIsLoading( false ); + } ); + }, [ jetpackSettings ] ); + + // Handle auto-save for newsletter toggle and email settings + const handleAutoSave = useCallback( + ( updates: Partial< NewsletterSettings > ) => { + if ( ! data ) { + return; + } + + // Update local state optimistically + setData( { ...data, ...updates } ); + + // Save to backend + restApi.updateSettings( updates ).catch( ( err: Error ) => { + setError( err.message || 'Failed to save settings' ); + // Revert optimistic update on error + setData( data ); + } ); + }, + [ data ] + ); + + // Handle subscription settings changes (staged, not auto-saved) + const handleSubscriptionChange = useCallback( + ( updates: Partial< NewsletterSettings > ) => { + if ( ! data ) { + return; + } + + // Update local state + setData( { ...data, ...updates } ); + + // Track changes for save button + setSubscriptionChanges( { ...subscriptionChanges, ...updates } ); + }, + [ data, subscriptionChanges ] + ); + + // Save subscription settings + const saveSubscriptionSettings = useCallback( () => { + setIsSavingSubscriptions( true ); + setError( null ); + + restApi + .updateSettings( subscriptionChanges ) + .then( () => { + setSubscriptionChanges( {} ); + setSnackbarMessage( __( 'Settings saved', 'jetpack-newsletter' ) ); + } ) + .catch( ( err: Error ) => { + setError( err.message || 'Failed to save subscription settings' ); + } ) + .finally( () => { + setIsSavingSubscriptions( false ); + } ); + }, [ subscriptionChanges ] ); + + // Handle sender name changes (staged, not auto-saved) + const handleSenderNameChange = useCallback( + ( updates: Partial< NewsletterSettings > ) => { + if ( ! data ) { + return; + } + + // Merge updates into staged changes + setSenderNameChanges( { ...senderNameChanges, ...updates } ); + }, + [ data, senderNameChanges ] + ); + + // Save sender name + const saveSenderName = useCallback( () => { + if ( ! data ) { + return; + } + + setIsSavingSenderName( true ); + setError( null ); + + restApi + .updateSettings( senderNameChanges ) + .then( () => { + setData( { ...data, ...senderNameChanges } ); + setSenderNameChanges( {} ); + setSnackbarMessage( __( 'Sender name saved', 'jetpack-newsletter' ) ); + } ) + .catch( ( err: Error ) => { + setError( err.message || 'Failed to save sender name' ); + } ) + .finally( () => { + setIsSavingSenderName( false ); + } ); + }, [ senderNameChanges, data ] ); + + // Handle newsletter categories changes (staged, not auto-saved) + const handleNewsletterCategoriesChange = useCallback( + ( updates: Partial< NewsletterSettings > ) => { + if ( ! data ) { + return; + } + + // Update local state + setData( { ...data, ...updates } ); + + // Track changes for save button + setNewsletterCategoriesChanges( { ...newsletterCategoriesChanges, ...updates } ); + }, + [ data, newsletterCategoriesChanges ] + ); + + // Save newsletter categories settings + const saveNewsletterCategories = useCallback( () => { + if ( ! data ) { + return; + } + + setIsSavingNewsletterCategories( true ); + setError( null ); + + // Convert categories from strings to numbers for API + const apiUpdates: Record< string, unknown > = { ...newsletterCategoriesChanges }; + if ( apiUpdates.wpcom_newsletter_categories ) { + apiUpdates.wpcom_newsletter_categories = ( + apiUpdates.wpcom_newsletter_categories as string[] + ).map( Number ); + } + + restApi + .updateSettings( apiUpdates ) + .then( () => { + setNewsletterCategoriesChanges( {} ); + setSnackbarMessage( __( 'Newsletter categories saved', 'jetpack-newsletter' ) ); + } ) + .catch( ( err: Error ) => { + setError( err.message || 'Failed to save newsletter categories' ); + } ) + .finally( () => { + setIsSavingNewsletterCategories( false ); + } ); + }, [ newsletterCategoriesChanges, data ] ); + + // Handle welcome email changes (staged, not auto-saved) + const handleWelcomeEmailChange = useCallback( + ( updates: Partial< NewsletterSettings > ) => { + if ( ! data ) { + return; + } + + // Update local state + setData( { ...data, ...updates } ); + + // Track changes for save button + setWelcomeEmailChanges( { ...welcomeEmailChanges, ...updates } ); + }, + [ data, welcomeEmailChanges ] + ); + + // Save welcome email settings + const saveWelcomeEmail = useCallback( () => { + if ( ! data ) { + return; + } + + setIsSavingWelcomeEmail( true ); + setError( null ); + + restApi + .updateSettings( welcomeEmailChanges ) + .then( () => { + setWelcomeEmailChanges( {} ); + setSnackbarMessage( __( 'Welcome email message saved', 'jetpack-newsletter' ) ); + } ) + .catch( ( err: Error ) => { + setError( err.message || 'Failed to save welcome email message' ); + } ) + .finally( () => { + setIsSavingWelcomeEmail( false ); + } ); + }, [ welcomeEmailChanges, data ] ); + + if ( isLoading ) { + return ( +
+

{ __( 'Loading newsletter settings…', 'jetpack-newsletter' ) }

+
+ ); + } + + if ( error ) { + return ( +
+ + { error } + +
+ ); + } + + if ( ! data ) { + return null; + } + + const hasSubscriptionChanges = Object.keys( subscriptionChanges ).length > 0; + const hasSenderNameChanges = Object.keys( senderNameChanges ).length > 0; + const hasNewsletterCategoriesChanges = Object.keys( newsletterCategoriesChanges ).length > 0; + const hasWelcomeEmailChanges = Object.keys( welcomeEmailChanges ).length > 0; + return (
-

Newsletter Settings

-

This is a proof of concept, I am rendered via React.

+
+ + { ! jetpackSettings?.isWpcomSimple && ( + + ) } + + + + + + + + + + + + + + + + + + { snackbarMessage && { snackbarMessage } }
); } -// Initialize the app when DOM is ready const container = document.getElementById( 'newsletter-settings-root' ); if ( container ) { const root = createRoot( container ); diff --git a/projects/packages/newsletter/src/settings/sections/email-byline-section.tsx b/projects/packages/newsletter/src/settings/sections/email-byline-section.tsx new file mode 100644 index 0000000000000..c0175b546724e --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/email-byline-section.tsx @@ -0,0 +1,123 @@ +/** + * External dependencies + */ +import { DataForm, type Field } from '@wordpress/dataviews/wp'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { BylinePreview } from '../components/byline-preview'; +import { ToggleWithLink } from '../components/toggle-with-link'; +import type { NewsletterSettings, JetpackNewsletterSettings } from '../types'; + +interface EmailBylineSectionProps { + data: NewsletterSettings; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + jetpackSettings: JetpackNewsletterSettings | undefined; + isNewsletterEnabled: boolean; +} + +/** + * Email Byline Section Component + * + * Handles the email byline settings (gravatar, author, date) with live preview. + * + * @param {EmailBylineSectionProps} props - Component props + * @return {JSX.Element} The email byline section + */ +export function EmailBylineSection( { + data, + onChange, + jetpackSettings, + isNewsletterEnabled, +}: EmailBylineSectionProps ): JSX.Element { + const fields: Field< NewsletterSettings >[] = [ + { + id: 'jetpack_gravatar_in_email', + label: __( 'Show author avatar on your emails', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: jetpackSettings?.email + ? ( { data: fieldData, field, onChange: fieldOnChange } ) => ( + } + field={ field as Field< Record< string, unknown > > } + onChange={ fieldOnChange } + url={ `https://gravatar.com/${ jetpackSettings.email }` } + linkText={ __( 'Update your Gravatar', 'jetpack-newsletter' ) } + /> + ) + : ( 'toggle' as const ), + description: __( + 'We use Gravatar, a service that associates an avatar image with your primary email address.', + 'jetpack-newsletter' + ), + }, + { + id: 'jetpack_author_in_email', + label: __( 'Show author display name', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: 'toggle' as const, + }, + { + id: 'jetpack_post_date_in_email', + label: __( 'Add the post date', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: jetpackSettings?.siteAdminUrl + ? ( { data: fieldData, field, onChange: fieldOnChange } ) => ( + } + field={ field as Field< Record< string, unknown > > } + onChange={ fieldOnChange } + url={ `${ jetpackSettings.siteAdminUrl }options-general.php` } + linkText={ __( 'Customize date format', 'jetpack-newsletter' ) } + isExternal={ false } + /> + ) + : ( 'toggle' as const ), + }, + ]; + + return ( +
+

+ { __( 'Email byline', 'jetpack-newsletter' ) } +

+

+ { __( + 'Customize the information you want to display below your post title in emails.', + 'jetpack-newsletter' + ) } +

+
+ + + { /* Byline Preview - positioned right after the toggles */ } + { jetpackSettings && ( + + ) } +
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/sections/email-content-section.tsx b/projects/packages/newsletter/src/settings/sections/email-content-section.tsx new file mode 100644 index 0000000000000..5771843329606 --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/email-content-section.tsx @@ -0,0 +1,92 @@ +/** + * External dependencies + */ +import { Notice } from '@wordpress/components'; +import { DataForm, type Field } from '@wordpress/dataviews/wp'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import type { NewsletterSettings } from '../types'; + +interface EmailContentSectionProps { + data: NewsletterSettings; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + isSitePublic: boolean; + isNewsletterEnabled: boolean; +} + +/** + * Email Content Section Component + * + * Handles featured image and full text/excerpt settings for newsletter emails. + * + * @param {EmailContentSectionProps} props - Component props + * @return {JSX.Element} The email content section + */ +export function EmailContentSection( { + data, + onChange, + isSitePublic, + isNewsletterEnabled, +}: EmailContentSectionProps ): JSX.Element { + const fields: Field< NewsletterSettings >[] = [ + { + id: 'wpcom_featured_image_in_email', + label: __( "Include the post's featured image in the new post emails", 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: 'toggle' as const, + }, + { + id: 'wpcom_subscription_emails_use_excerpt', + label: __( 'For each new post email, include', 'jetpack-newsletter' ), + type: 'integer' as const, + Edit: 'radio' as const, + elements: [ + { + value: 0, + label: __( 'Full text', 'jetpack-newsletter' ), + }, + { + value: 1, + label: __( 'Excerpt', 'jetpack-newsletter' ), + }, + ], + description: __( + 'Sets whether email subscribers can read full posts in emails or just an excerpt and link to the full version of the post.', + 'jetpack-newsletter' + ), + }, + ]; + + return ( +
+

+ { __( 'Email content', 'jetpack-newsletter' ) } +

+
+ { ! isSitePublic && ( + + { __( + 'Featured images will not be used in your emails while the site is private, because access to the images is restricted to your site only.', + 'jetpack-newsletter' + ) } + + ) } + + +
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/sections/email-reply-to-settings-section.tsx b/projects/packages/newsletter/src/settings/sections/email-reply-to-settings-section.tsx new file mode 100644 index 0000000000000..2928ec48878ee --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/email-reply-to-settings-section.tsx @@ -0,0 +1,75 @@ +/** + * External dependencies + */ +import { DataForm, type Field } from '@wordpress/dataviews/wp'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import type { NewsletterSettings } from '../types'; + +interface EmailReplyToSettingsSectionProps { + data: NewsletterSettings; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + isNewsletterEnabled: boolean; +} + +/** + * Email Reply-to Settings Section Component + * + * Handles the reply-to configuration for newsletter emails. + * + * @param {EmailReplyToSettingsSectionProps} props - Component props + * @return {JSX.Element} The email reply-to settings section + */ +export function EmailReplyToSettingsSection( { + data, + onChange, + isNewsletterEnabled, +}: EmailReplyToSettingsSectionProps ): JSX.Element { + const fields: Field< NewsletterSettings >[] = [ + { + id: 'jetpack_subscriptions_reply_to', + label: __( 'Reply-to settings', 'jetpack-newsletter' ), + type: 'text' as const, + Edit: 'radio' as const, + elements: [ + { + value: 'comment', + label: __( 'Replies will be a public comment on the post', 'jetpack-newsletter' ), + }, + { + value: 'author', + label: __( "Replies will be sent to the post author's email", 'jetpack-newsletter' ), + }, + { value: 'no-reply', label: __( 'Replies are not allowed', 'jetpack-newsletter' ) }, + ], + description: __( + "Choose who receives emails when subscribers reply to your newsletter. The author's account must be connected to WordPress.com to use their email as the reply-to address.", + 'jetpack-newsletter' + ), + }, + ]; + + return ( +
+

+ { __( 'Reply-to settings', 'jetpack-newsletter' ) } +

+
+ +
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/sections/email-sender-settings-section.tsx b/projects/packages/newsletter/src/settings/sections/email-sender-settings-section.tsx new file mode 100644 index 0000000000000..4f801256c3ad0 --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/email-sender-settings-section.tsx @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import { DataForm, type Field } from '@wordpress/dataviews/wp'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import type { NewsletterSettings, JetpackNewsletterSettings } from '../types'; + +interface EmailSenderSettingsSectionProps { + data: NewsletterSettings; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + onSave: () => void; + isSaving: boolean; + hasChanges: boolean; + jetpackSettings: JetpackNewsletterSettings | undefined; + isNewsletterEnabled: boolean; +} + +/** + * Email Sender Settings Section Component + * + * Handles the sender name configuration for newsletter emails. + * + * @param {EmailSenderSettingsSectionProps} props - Component props + * @return {JSX.Element} The email sender settings section + */ +export function EmailSenderSettingsSection( { + data, + onChange, + onSave, + isSaving, + hasChanges, + jetpackSettings, + isNewsletterEnabled, +}: EmailSenderSettingsSectionProps ): JSX.Element { + // Translation strings for save button + const savingText = __( 'Saving…', 'jetpack-newsletter' ); + const saveText = __( 'Save', 'jetpack-newsletter' ); + + const fields: Field< NewsletterSettings >[] = [ + { + id: 'jetpack_subscriptions_from_name', + label: __( 'Sender name', 'jetpack-newsletter' ), + type: 'text' as const, + description: __( + "This is the name that appears in subscribers' inboxes. It's usually the name of your newsletter or the author.", + 'jetpack-newsletter' + ), + }, + ]; + + // Get the current sender name value + const senderName = data.jetpack_subscriptions_from_name || ''; + + return ( +
+

+ { __( 'Sender settings', 'jetpack-newsletter' ) } +

+
+ + + { /* Inline preview of how the sender name appears in email */ } +
+

+ { __( 'Preview:', 'jetpack-newsletter' ) }{ ' ' } + + { senderName || + jetpackSettings?.displayName || + __( 'Your Name', 'jetpack-newsletter' ) } + { ' ' } + <comment-reply@wordpress.com> +

+
+ +
+ +
+
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/sections/index.ts b/projects/packages/newsletter/src/settings/sections/index.ts new file mode 100644 index 0000000000000..e97af8a642106 --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/index.ts @@ -0,0 +1,12 @@ +/** + * Export all section components + */ +export { EmailContentSection } from './email-content-section'; +export { EmailBylineSection } from './email-byline-section'; +export { EmailSenderSettingsSection } from './email-sender-settings-section'; +export { EmailReplyToSettingsSection } from './email-reply-to-settings-section'; +export { NewsletterSection } from './newsletter-section'; +export { NewsletterCategoriesSection } from './newsletter-categories-section'; +export { PaidNewsletterSection } from './paid-newsletter-section'; +export { SubscriptionsSection } from './subscriptions-section'; +export { WelcomeEmailSection } from './welcome-email-section'; diff --git a/projects/packages/newsletter/src/settings/sections/newsletter-categories-section.tsx b/projects/packages/newsletter/src/settings/sections/newsletter-categories-section.tsx new file mode 100644 index 0000000000000..4fdc701ba1a0b --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/newsletter-categories-section.tsx @@ -0,0 +1,214 @@ +/** + * External dependencies + */ +import { WpcomSupportLink } from '@automattic/jetpack-shared-extension-utils/components'; +import { Button, ExternalLink } from '@wordpress/components'; +import { DataForm, type Field, useFormValidity } from '@wordpress/dataviews/wp'; +import { createInterpolateElement, useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import type { NewsletterSettings, JetpackNewsletterSettings, WordPressCategory } from '../types'; + +interface NewsletterCategoriesSectionProps { + data: NewsletterSettings; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + onSave: () => void; + isSaving: boolean; + hasChanges: boolean; + jetpackSettings: JetpackNewsletterSettings | undefined; + onError: ( error: string ) => void; + isNewsletterEnabled: boolean; +} + +/** + * Newsletter Categories Section Component + * + * @param {NewsletterCategoriesSectionProps} props - Component props + * @return {JSX.Element} The newsletter categories section + */ +export function NewsletterCategoriesSection( { + data, + onChange, + onSave, + isSaving, + hasChanges, + jetpackSettings, + onError, + isNewsletterEnabled, +}: NewsletterCategoriesSectionProps ): JSX.Element { + const [ categories, setCategories ] = useState< WordPressCategory[] >( [] ); + const [ isFetchingCategories, setIsFetchingCategories ] = useState( true ); + + // Fetch WordPress categories on mount + useEffect( () => { + const wpApiSettings = ( window as Window & { wpApiSettings?: { root: string; nonce: string } } ) + .wpApiSettings; + + if ( ! wpApiSettings?.root ) { + setIsFetchingCategories( false ); + return; + } + + // Fetch categories from WordPress REST API + fetch( `${ wpApiSettings.root }wp/v2/categories?per_page=100`, { + headers: { + 'X-WP-Nonce': wpApiSettings.nonce, + }, + } ) + .then( response => { + if ( ! response.ok ) { + throw new Error( + `Failed to load categories: ${ response.status } ${ response.statusText }` + ); + } + return response.json(); + } ) + .then( ( fetchedCategories: { id: number; name: string }[] ) => { + // Convert category IDs to strings + setCategories( + fetchedCategories.map( cat => ( { + id: String( cat.id ), + name: cat.name, + } ) ) + ); + setIsFetchingCategories( false ); + } ) + .catch( ( err: Error ) => { + onError( err.message || __( 'Failed to load categories', 'jetpack-newsletter' ) ); + setIsFetchingCategories( false ); + } ); + }, [ onError ] ); + + // Define fields + const fields: Field< NewsletterSettings >[] = [ + { + id: 'wpcom_newsletter_categories_enabled', + label: __( 'Enable newsletter categories', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: 'toggle' as const, + }, + { + id: 'wpcom_newsletter_categories', + label: __( + 'Which categories will you use for newsletter subscribers? Select all that apply:', + 'jetpack-newsletter' + ), + type: 'array' as const, + elements: categories.map( cat => ( { + value: cat.id, + label: cat.name, + } ) ), + isValid: { + elements: true, + custom: ( item: NewsletterSettings ) => { + if ( + item.wpcom_newsletter_categories_enabled && + ! item.wpcom_newsletter_categories?.length + ) { + return __( + 'Please select at least one category when newsletter categories are enabled.', + 'jetpack-newsletter' + ); + } + return null; + }, + }, + }, + ]; + + // Field list for newsletter categories section + const newsletterCategoriesFieldIds = data.wpcom_newsletter_categories_enabled + ? [ 'wpcom_newsletter_categories_enabled', 'wpcom_newsletter_categories' ] + : [ 'wpcom_newsletter_categories_enabled' ]; + + const newsletterCategoriesFields = fields.filter( f => + newsletterCategoriesFieldIds.includes( f.id ) + ); + + // Form configuration for newsletter categories + const newsletterCategoriesForm = { + layout: { + type: 'regular' as const, + labelPosition: 'top' as const, + }, + fields: newsletterCategoriesFieldIds, + }; + + // Get form validity state for newsletter categories + const { validity = {}, isValid = true } = + useFormValidity( data, newsletterCategoriesFields, newsletterCategoriesForm ) || {}; + + // Translation strings for save button + const savingText = __( 'Saving…', 'jetpack-newsletter' ); + const saveText = __( 'Save', 'jetpack-newsletter' ); + + // Build subscribe block documentation URL and component + const subscribeBlockUrl = jetpackSettings?.isWpcomPlatform + ? 'https://wordpress.com/support/wordpress-editor/blocks/subscribe-block/' + : `https://jetpack.com/redirect/?source=jetpack-support-subscribe-block&site=${ + jetpackSettings?.blogID || '' + }`; + + const SubscribeBlockLink = jetpackSettings?.isWpcomPlatform ? ( + + ) : ( + + ); + + return ( +
+

+ { __( 'Newsletter categories', 'jetpack-newsletter' ) } +

+

+ { createInterpolateElement( + __( + "Newsletter categories let you select the content that's emailed to subscribers. When enabled, only posts in the selected categories will be sent as newsletters. By default, subscribers can choose from your selected categories, or you can pre-select categories using the subscribe block. When you add a new category, your existing subscribers will be automatically subscribed to it.", + 'jetpack-newsletter' + ), + { + link: SubscribeBlockLink, + } + ) } +

+
+ + + { data.wpcom_newsletter_categories_enabled && jetpackSettings?.siteAdminUrl && ( +
+ + { __( 'Add New Category', 'jetpack-newsletter' ) } + +
+ ) } + +
+ +
+
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/sections/newsletter-section.tsx b/projects/packages/newsletter/src/settings/sections/newsletter-section.tsx new file mode 100644 index 0000000000000..48b4a807c2508 --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/newsletter-section.tsx @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import { ExternalLink } from '@wordpress/components'; +import { DataForm, type Field } from '@wordpress/dataviews/wp'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { getManageSubscribersUrl } from '../utils'; +import type { NewsletterSettings, JetpackNewsletterSettings } from '../types'; + +interface NewsletterSectionProps { + data: NewsletterSettings; + jetpackSettings: JetpackNewsletterSettings | undefined; + onChange: ( updates: Partial< NewsletterSettings > ) => void; +} + +/** + * Newsletter Section Component + * + * @param {NewsletterSectionProps} props - Component props + * @return {JSX.Element} The newsletter section + */ +export function NewsletterSection( { + data, + jetpackSettings, + onChange, +}: NewsletterSectionProps ): JSX.Element { + const fields: Field< NewsletterSettings >[] = [ + { + id: 'subscriptions', + label: __( + 'Let visitors subscribe to this site and receive emails when you publish a post', + 'jetpack-newsletter' + ), + type: 'boolean' as const, + Edit: 'toggle' as const, + }, + ]; + + return ( +
+

+ { __( 'Newsletter', 'jetpack-newsletter' ) } +

+
+ + { data.subscriptions && ( +
+ + { __( 'Manage all subscribers', 'jetpack-newsletter' ) } + +
+ ) } +
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/sections/paid-newsletter-section.tsx b/projects/packages/newsletter/src/settings/sections/paid-newsletter-section.tsx new file mode 100644 index 0000000000000..ce9b6b7c8493a --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/paid-newsletter-section.tsx @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import type { JetpackNewsletterSettings } from '../types'; + +interface PaidNewsletterSectionProps { + jetpackSettings: JetpackNewsletterSettings | undefined; + isNewsletterEnabled: boolean; +} + +/** + * Paid Newsletter Section Component + * + * @param {PaidNewsletterSectionProps} props - Component props + * @return {JSX.Element | null} The paid newsletter section or null if URL not available + */ +export function PaidNewsletterSection( { + jetpackSettings, + isNewsletterEnabled, +}: PaidNewsletterSectionProps ): JSX.Element | null { + if ( ! jetpackSettings?.setupPaymentPlansUrl ) { + return null; + } + + return ( +
+

+ { __( 'Paid newsletter', 'jetpack-newsletter' ) } +

+

+ { __( + 'Earn money through your Newsletter. Reward your most loyal subscribers with exclusive content or add a paywall to monetize content.', + 'jetpack-newsletter' + ) } +

+
+ +
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/sections/subscriptions-section.tsx b/projects/packages/newsletter/src/settings/sections/subscriptions-section.tsx new file mode 100644 index 0000000000000..eca234eda3334 --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/subscriptions-section.tsx @@ -0,0 +1,293 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import { DataForm, type Field } from '@wordpress/dataviews/wp'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { ToggleWithEditorLink } from '../components/toggle-with-link'; +import type { NewsletterSettings, JetpackNewsletterSettings } from '../types'; + +interface SubscriptionsSectionProps { + data: NewsletterSettings; + jetpackSettings: JetpackNewsletterSettings | undefined; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + onSave: () => void; + isSaving: boolean; + hasChanges: boolean; + isNewsletterEnabled: boolean; +} + +/** + * Subscriptions Section Component + * + * @param {SubscriptionsSectionProps} props - Component props + * @return {JSX.Element} The subscriptions section + */ +export function SubscriptionsSection( { + data, + jetpackSettings, + onChange, + onSave, + isSaving, + hasChanges, + isNewsletterEnabled, +}: SubscriptionsSectionProps ): JSX.Element { + // Translation strings for save button + const savingText = __( 'Saving…', 'jetpack-newsletter' ); + const saveText = __( 'Save', 'jetpack-newsletter' ); + + // Helper to check if we can show editor links for block theme features + const canShowBlockThemeEditorLinks = + jetpackSettings?.isBlockTheme && + jetpackSettings?.siteAdminUrl && + jetpackSettings?.themeStylesheet; + + // Helper to check if we can show editor links for subscription site edit features + const canShowSubscriptionEditorLinks = + jetpackSettings?.isSubscriptionSiteEditSupported && + jetpackSettings?.siteAdminUrl && + jetpackSettings?.themeStylesheet; + + const fields: Field< NewsletterSettings >[] = [ + { + id: 'jetpack_subscriptions_subscribe_post_end_enabled', + label: __( 'Add the Subscribe Block at the end of each post.', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: canShowSubscriptionEditorLinks + ? ( { + data: formData, + field, + onChange: fieldOnChange, + }: { + data: NewsletterSettings; + field: Field< Record< string, unknown > >; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + } ) => ( + + ) + : ( 'toggle' as const ), + }, + { + id: 'sm_enabled', + label: __( 'Show subscription pop-up when scrolling a post.', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: canShowBlockThemeEditorLinks + ? ( { + data: formData, + field, + onChange: fieldOnChange, + }: { + data: NewsletterSettings; + field: Field< Record< string, unknown > >; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + } ) => ( + + ) + : ( 'toggle' as const ), + }, + { + id: 'jetpack_subscribe_overlay_enabled', + label: __( 'Subscription overlay on homepage', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: canShowBlockThemeEditorLinks + ? ( { + data: formData, + field, + onChange: fieldOnChange, + }: { + data: NewsletterSettings; + field: Field< Record< string, unknown > >; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + } ) => ( + + ) + : ( 'toggle' as const ), + }, + { + id: 'jetpack_subscribe_floating_button_enabled', + label: __( "Floating subscribe button on site's bottom corner", 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: canShowBlockThemeEditorLinks + ? ( { + data: formData, + field, + onChange: fieldOnChange, + }: { + data: NewsletterSettings; + field: Field< Record< string, unknown > >; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + } ) => ( + + ) + : ( 'toggle' as const ), + }, + { + id: 'jetpack_subscriptions_subscribe_navigation_enabled', + label: __( 'Add the Subscribe Block to the navigation', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: canShowSubscriptionEditorLinks + ? ( { + data: formData, + field, + onChange: fieldOnChange, + }: { + data: NewsletterSettings; + field: Field< Record< string, unknown > >; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + } ) => ( + + ) + : ( 'toggle' as const ), + }, + { + id: 'jetpack_subscriptions_login_navigation_enabled', + label: __( 'Add the Subscriber Login Block to the navigation', 'jetpack-newsletter' ), + type: 'boolean' as const, + Edit: canShowSubscriptionEditorLinks + ? ( { + data: formData, + field, + onChange: fieldOnChange, + }: { + data: NewsletterSettings; + field: Field< Record< string, unknown > >; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + } ) => ( + + ) + : ( 'toggle' as const ), + }, + { + id: 'stb_enabled', + label: __( + 'Enable the "Subscribe to site" option on your comment form', + 'jetpack-newsletter' + ), + type: 'boolean' as const, + Edit: 'toggle' as const, + }, + { + id: 'stc_enabled', + label: __( + 'Enable the "Subscribe to comments" option on your comment form', + 'jetpack-newsletter' + ), + type: 'boolean' as const, + Edit: 'toggle' as const, + }, + ]; + + return ( +
+

+ { __( 'Subscriptions', 'jetpack-newsletter' ) } +

+

+ { __( + 'Automatically add subscription forms to your site and turn visitors into subscribers.', + 'jetpack-newsletter' + ) } +

+
+ + +
+ +
+
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/sections/welcome-email-section.tsx b/projects/packages/newsletter/src/settings/sections/welcome-email-section.tsx new file mode 100644 index 0000000000000..ffd1773ea67d7 --- /dev/null +++ b/projects/packages/newsletter/src/settings/sections/welcome-email-section.tsx @@ -0,0 +1,122 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import { DataForm, type Field } from '@wordpress/dataviews/wp'; +import { useCallback, useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import type { NewsletterSettings } from '../types'; + +interface WelcomeEmailSectionProps { + data: NewsletterSettings; + onChange: ( updates: Partial< NewsletterSettings > ) => void; + onSave: () => void; + isSaving: boolean; + hasChanges: boolean; + isNewsletterEnabled: boolean; +} + +// Flattened data structure for DataForm +interface WelcomeEmailFormData { + welcome_message: string; +} + +/** + * Welcome Email Section Component + * + * Handles the welcome email message configuration for new subscribers. + * + * @param {WelcomeEmailSectionProps} props - Component props + * @return {JSX.Element} The welcome email section + */ +export function WelcomeEmailSection( { + data, + onChange, + onSave, + isSaving, + hasChanges, + isNewsletterEnabled, +}: WelcomeEmailSectionProps ): JSX.Element { + // Flatten data for DataForm + const formData: WelcomeEmailFormData = useMemo( + () => ( { + welcome_message: data.subscription_options?.welcome || '', + } ), + [ data.subscription_options?.welcome ] + ); + + // Translation strings for save button + const savingText = __( 'Saving…', 'jetpack-newsletter' ); + const saveText = __( 'Save', 'jetpack-newsletter' ); + + const fields: Field< WelcomeEmailFormData >[] = [ + { + id: 'welcome_message', + label: __( 'Welcome message', 'jetpack-newsletter' ), + type: 'text' as const, + Edit: 'textarea' as const, + description: __( + 'You can use plain text or HTML tags in this textarea for formatting.', + 'jetpack-newsletter' + ), + }, + ]; + + const handleDataFormChange = useCallback( + ( updates: Partial< WelcomeEmailFormData > ) => { + if ( updates.welcome_message !== undefined ) { + // Preserve all properties of subscription_options when updating + onChange( { + subscription_options: { + invitation: data.subscription_options?.invitation || '', + welcome: updates.welcome_message, + comment_follow: data.subscription_options?.comment_follow || '', + }, + } ); + } + }, + [ onChange, data.subscription_options ] + ); + + return ( +
+

+ { __( 'Welcome email message', 'jetpack-newsletter' ) } +

+

+ { __( + 'Sent to your email subscribers when they subscribe to your newsletter.', + 'jetpack-newsletter' + ) } +

+
+ + +
+ +
+
+
+ ); +} diff --git a/projects/packages/newsletter/src/settings/style.scss b/projects/packages/newsletter/src/settings/style.scss index 91745bc2630fd..73d7625cb8239 100644 --- a/projects/packages/newsletter/src/settings/style.scss +++ b/projects/packages/newsletter/src/settings/style.scss @@ -1,8 +1,84 @@ .newsletter-settings { - padding: 20px; + max-width: 1200px; + margin: 0 auto; + padding: 0; - h1 { - font-size: 24px; - margin-bottom: 16px; + // Card-based sections + &__section { + background: #fff; + border: 1px solid #c3c4c7; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + margin-bottom: 20px; + padding: 1.5em; + } + + &__section-title { + margin: 0; + padding: 0; + } + + &__section-description { + margin: 0; + padding: 0; + text-wrap: pretty; + } + + &__section-content { + padding-top: 1em; + + // Disabled state styling + &:disabled { + opacity: 0.5; + pointer-events: none; + position: relative; + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.6); + pointer-events: none; + } + } + } + + &__section-actions { + margin-top: 2em; + display: flex; + justify-content: flex-start; + } + + // Links styling + &__link { + margin-top: 2em; + } + + // Help text with links + &__help-text { + margin-top: 12px; + font-size: 13px; + } + + &--error { + padding: 20px; + + h2 { + color: #d63638; + font-size: 18px; + margin-bottom: 12px; + } + + p { + color: #646970; + } + } + + + // Notice component styling + .components-notice { + margin: 0 0 20px; } } diff --git a/projects/packages/newsletter/src/settings/types.ts b/projects/packages/newsletter/src/settings/types.ts new file mode 100644 index 0000000000000..18eaa3fa364cb --- /dev/null +++ b/projects/packages/newsletter/src/settings/types.ts @@ -0,0 +1,65 @@ +/** + * Type definitions for newsletter settings + */ + +/** + * Type definitions for newsletter settings data + */ +export interface NewsletterSettings { + subscriptions: boolean; + stb_enabled: boolean; + stc_enabled: boolean; + sm_enabled: boolean; + jetpack_subscribe_overlay_enabled: boolean; + jetpack_subscribe_floating_button_enabled: boolean; + jetpack_subscriptions_subscribe_post_end_enabled: boolean; + jetpack_subscriptions_login_navigation_enabled: boolean; + jetpack_subscriptions_subscribe_navigation_enabled: boolean; + wpcom_featured_image_in_email: boolean; + wpcom_subscription_emails_use_excerpt: boolean; + jetpack_gravatar_in_email: boolean; + jetpack_author_in_email: boolean; + jetpack_post_date_in_email: boolean; + jetpack_subscriptions_reply_to: 'comment' | 'author' | 'no-reply'; + jetpack_subscriptions_from_name: string; + wpcom_newsletter_categories_enabled: boolean; + wpcom_newsletter_categories: string[]; + subscription_options?: { + invitation: string; + welcome: string; + comment_follow: string; + }; + [ key: string ]: unknown; +} + +/** + * Type definitions for Jetpack Newsletter settings passed from PHP + */ +export interface JetpackNewsletterSettings { + isBlockTheme: boolean; + siteAdminUrl: string; + themeStylesheet: string; + blogID: number; + siteRawUrl: string; + email: string; + gravatar: string; + displayName: string; + dateExample: string; + wpAdminSubscriberManagementEnabled: boolean; + isSubscriptionSiteEditSupported: boolean; + setupPaymentPlansUrl: string; + isSitePublic: boolean; + isWpcomPlatform: boolean; + isWpcomSimple: boolean; + isSubscriptionsActive: boolean; + restApiRoot: string; + restApiNonce: string; +} + +/** + * Type definition for WordPress category + */ +export interface WordPressCategory { + id: string; + name: string; +} diff --git a/projects/packages/newsletter/src/settings/utils.ts b/projects/packages/newsletter/src/settings/utils.ts new file mode 100644 index 0000000000000..31bb72d25181a --- /dev/null +++ b/projects/packages/newsletter/src/settings/utils.ts @@ -0,0 +1,26 @@ +/** + * Utility functions for newsletter settings + */ +import type { JetpackNewsletterSettings } from './types'; + +/** + * Helper function to get "Manage all subscribers" URL + * + * @param {JetpackNewsletterSettings | undefined} jetpackSettings - Settings passed from PHP + * @return {string} URL to manage subscribers + */ +export function getManageSubscribersUrl( + jetpackSettings: JetpackNewsletterSettings | undefined +): string { + if ( ! jetpackSettings ) { + return '#'; + } + + if ( jetpackSettings.wpAdminSubscriberManagementEnabled ) { + return `${ jetpackSettings.siteAdminUrl }admin.php?page=subscribers`; + } + + // Fallback to WordPress.com URL (prefer siteRawUrl, fallback to blogID) + const site = jetpackSettings.siteRawUrl || jetpackSettings.blogID; + return `https://wordpress.com/subscribers/${ site }`; +} diff --git a/projects/packages/newsletter/webpack.config.js b/projects/packages/newsletter/webpack.config.js index 85c1e7abd4aca..ae224f5cbf93d 100644 --- a/projects/packages/newsletter/webpack.config.js +++ b/projects/packages/newsletter/webpack.config.js @@ -68,6 +68,17 @@ export default { // Transpile JavaScript and TypeScript jetpackWebpackConfig.TranspileRule( { exclude: /node_modules\//, + babelOpts: { + configFile: false, + plugins: [ + [ + require.resolve( '@automattic/babel-plugin-replace-textdomain' ), + { + textdomain: 'jetpack-newsletter', + }, + ], + ], + }, } ), // Transpile @automattic/* in node_modules too. diff --git a/projects/plugins/jetpack/changelog/jetpack-newsletter-wip b/projects/plugins/jetpack/changelog/jetpack-newsletter-wip new file mode 100644 index 0000000000000..ad71c2e7e621a --- /dev/null +++ b/projects/plugins/jetpack/changelog/jetpack-newsletter-wip @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Newsletter: Work on new UI, not available yet. diff --git a/projects/plugins/jetpack/load-jetpack.php b/projects/plugins/jetpack/load-jetpack.php index a402eb4624c24..c13d895f9257b 100644 --- a/projects/plugins/jetpack/load-jetpack.php +++ b/projects/plugins/jetpack/load-jetpack.php @@ -61,6 +61,9 @@ function jetpack_should_use_minified_assets() { if ( is_admin() ) { require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; require_once JETPACK__PLUGIN_DIR . '_inc/lib/debugger.php'; + + // Initialize Newsletter Settings (always-loaded so the settings page URL works even when module is inactive). + \Automattic\Jetpack\Newsletter\Settings::init(); } // Play nice with https://wp-cli.org/.