diff --git a/includes/class-newspack-newsletters.php b/includes/class-newspack-newsletters.php index e2574b322..020f5ded8 100644 --- a/includes/class-newspack-newsletters.php +++ b/includes/class-newspack-newsletters.php @@ -160,6 +160,12 @@ public static function enqueue_block_editor_assets() { return; } + $mailchimp_api_key = self::mailchimp_api_key(); + $mjml_api_key = get_option( 'newspack_newsletters_mjml_api_key', false ); + $mjml_api_secret = get_option( 'newspack_newsletters_mjml_api_secret', false ); + + $has_keys = ! empty( $mailchimp_api_key ) && ! empty( $mjml_api_key ) && ! empty( $mjml_api_secret ); + \wp_enqueue_script( 'newspack-newsletters', plugins_url( '../dist/editor.js', __FILE__ ), @@ -173,6 +179,7 @@ public static function enqueue_block_editor_assets() { 'newspack_newsletters_data', [ 'templates' => self::get_newsletter_templates(), + 'has_keys' => $has_keys, ] ); @@ -275,6 +282,123 @@ public static function rest_api_init() { ], ] ); + \register_rest_route( + 'newspack-newsletters/v1/', + 'keys', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ __CLASS__, 'api_get_keys' ], + 'permission_callback' => [ __CLASS__, 'api_permissions_check' ], + ] + ); + \register_rest_route( + 'newspack-newsletters/v1/', + 'keys', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ __CLASS__, 'api_set_keys' ], + 'permission_callback' => [ __CLASS__, 'api_permissions_check' ], + 'args' => [ + 'mailchimp_api_key' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + 'mjml_application_id' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + 'mjml_api_secret' => [ + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ] + ); + } + + /** + * Retrieve API keys. + */ + public static function api_get_keys() { + $mailchimp_api_key = self::mailchimp_api_key(); + $mjml_api_key = get_option( 'newspack_newsletters_mjml_api_key', false ); + $mjml_api_secret = get_option( 'newspack_newsletters_mjml_api_secret', false ); + + $keys = [ + 'mailchimp_api_key' => $mailchimp_api_key, + 'mjml_api_key' => $mjml_api_key, + 'mjml_api_secret' => $mjml_api_secret, + 'status' => ! empty( $mailchimp_api_key ) && ! empty( $mjml_api_key ) && ! empty( $mjml_api_secret ), + ]; + return \rest_ensure_response( $keys ); + } + + /** + * Set API keys. + * + * @param WP_REST_Request $request API request object. + */ + public static function api_set_keys( $request ) { + $mailchimp_api_key = $request['mailchimp_api_key']; + $mjml_api_key = $request['mjml_api_key']; + $mjml_api_secret = $request['mjml_api_secret']; + $wp_error = new WP_Error(); + + $errors = []; + + if ( empty( $mailchimp_api_key ) ) { + $wp_error->add( + 'newspack_newsletters_invalid_keys_mailchimp', + __( 'Please input a Mailchimp API key.', 'newspack-newsletters' ) + ); + } else { + try { + $mc = new Mailchimp( $mailchimp_api_key ); + $ping = $mc->get( 'ping' ); + } catch ( Exception $e ) { + $ping = null; + } + if ( $ping ) { + update_option( 'newspack_newsletters_mailchimp_api_key', $mailchimp_api_key ); + } else { + $wp_error->add( + 'newspack_newsletters_invalid_keys_mailchimp', + __( 'Please input a valid Mailchimp API key.', 'newspack-newsletters' ) + ); + } + } + + if ( empty( $mjml_api_key ) || empty( $mjml_api_secret ) ) { + $wp_error->add( + 'newspack_newsletters_invalid_keys_mjml', + __( 'Please input MJML application ID and secret key.', 'newspack-newsletters' ) + ); + } else { + $credentials = "$mjml_api_key:$mjml_api_secret"; + $url = 'https://api.mjml.io/v1/render'; + $mjml_test = wp_remote_post( + $url, + [ + 'body' => wp_json_encode( + [ + 'mjml' => '

test

', + ] + ), + 'headers' => array( + 'Authorization' => 'Basic ' . base64_encode( $credentials ), + ), + 'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ] + ); + if ( 200 === $mjml_test['response']['code'] ) { + update_option( 'newspack_newsletters_mjml_api_key', $mjml_api_key ); + update_option( 'newspack_newsletters_mjml_api_secret', $mjml_api_secret ); + } else { + $wp_error->add( + 'newspack_newsletters_invalid_keys_mjml', + __( 'Please input valid MJML application ID and secret key.', 'newspack-newsletters' ) + ); + } + } + + return $wp_error->has_errors() ? $wp_error : self::api_get_keys(); } /** diff --git a/src/components/template-modal/index.js b/src/components/template-modal/index.js index f080e4985..8fd461fa2 100644 --- a/src/components/template-modal/index.js +++ b/src/components/template-modal/index.js @@ -5,88 +5,35 @@ /** * WordPress dependencies */ -import { parse } from '@wordpress/blocks'; -import { __, sprintf } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; -import { Button, Modal } from '@wordpress/components'; -import { ENTER, SPACE } from '@wordpress/keycodes'; -import { BlockPreview } from '@wordpress/block-editor'; +import { Modal } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ +import TemplatePicker from './screens/template-picker'; +import APIKeys from './screens/api-keys'; import './style.scss'; -class TemplateModal extends Component { - generateBlockPreview = () => { - const { selectedTemplate, templates } = this.props; - return templates && templates[ selectedTemplate ] - ? parse( templates[ selectedTemplate ].content ) - : null; - }; - render = () => { - const { onInsertTemplate, onSelectTemplate, selectedTemplate, templates } = this.props; - const blockPreview = this.generateBlockPreview(); - return ( - -
-
-
- { ( templates || [] ).map( ( { title, content }, index ) => ( -
onSelectTemplate( index ) } - onKeyDown={ event => { - if ( ENTER === event.keyCode || SPACE === event.keyCode ) { - event.preventDefault(); - onSelectTemplate( index ); - } - } } - role="button" - tabIndex="0" - aria-label={ title } - > -
- -
-
{ title }
-
- ) ) } -
-
- -
- { blockPreview && blockPreview.length > 0 ? ( - - ) : ( -

{ __( 'Select a layout to preview.', 'newspack-newsletters' ) }

- ) } -
-
- - { selectedTemplate !== null && ( - - ) } -
- ); - }; -} - -export default TemplateModal; +export default ( { hasKeys, onInsertTemplate, onSetupStatus, templates } ) => { + return ( + + { hasKeys ? ( + + ) : ( + + ) } + + ); +}; diff --git a/src/components/template-modal/screens/api-keys/index.js b/src/components/template-modal/screens/api-keys/index.js new file mode 100644 index 000000000..fb5f2e2a6 --- /dev/null +++ b/src/components/template-modal/screens/api-keys/index.js @@ -0,0 +1,117 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { Button, ExternalLink, Spinner, TextControl } from '@wordpress/components'; +import { Fragment, useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { ENTER } from '@wordpress/keycodes'; + +export default ( { onSetupStatus } ) => { + const [ keys, setKeys ] = useState( {} ); + const [ inFlight, setInFlight ] = useState( false ); + const [ errors, setErrors ] = useState( {} ); + const commitSettings = () => { + setInFlight( true ); + setErrors( {} ); + apiFetch( { + path: '/newspack-newsletters/v1/keys', + method: 'POST', + data: keys, + } ) + .then( results => { + setInFlight( false ); + setKeys( results ); + onSetupStatus( results.status ); + } ) + .catch( handleErrors ); + }; + const handleErrors = error => { + const allErrors = { [ error.code ]: error.message }; + ( error.additional_errors || [] ).forEach( + additionalError => ( allErrors[ additionalError.code ] = additionalError.message ) + ); + setErrors( allErrors ); + setInFlight( false ); + }; + useEffect(() => { + setInFlight( true ); + apiFetch( { path: `/newspack-newsletters/v1/keys` } ) + .then( results => { + setInFlight( false ); + setKeys( results ); + onSetupStatus( results.status ); + } ) + .catch( handleErrors ); + }, []); + + const { + mailchimp_api_key: mailchimpAPIKey = '', + mjml_api_key: mjmlApplicationId = '', + mjml_api_secret: mjmlAPISecret = '', + } = keys; + return ( + +
+
+

{ __( 'Enter your Mailchimp API key', 'newspack-newsletters' ) }

+ { errors.newspack_newsletters_invalid_keys_mailchimp && ( +

{ errors.newspack_newsletters_invalid_keys_mailchimp }

+ ) } + setKeys( { ...keys, mailchimp_api_key: value } ) } + disabled={ inFlight } + onKeyDown={ event => { + if ( ENTER === event.keyCode ) { + event.preventDefault(); + commitSettings(); + } + } } + /> + { inFlight && } + + { __( 'About Mailchimp API keys', 'newspack-newsletters' ) } + +

{ __( 'Enter your MJML API keys', 'newspack-newsletters' ) }

+ { errors.newspack_newsletters_invalid_keys_mjml && ( +

{ errors.newspack_newsletters_invalid_keys_mjml }

+ ) } + setKeys( { ...keys, mjml_api_key: value } ) } + disabled={ inFlight } + onKeyDown={ event => { + if ( ENTER === event.keyCode ) { + event.preventDefault(); + commitSettings(); + } + } } + /> + { inFlight && } + setKeys( { ...keys, mjml_api_secret: value } ) } + disabled={ inFlight } + onKeyDown={ event => { + if ( ENTER === event.keyCode ) { + event.preventDefault(); + commitSettings(); + } + } } + /> + { inFlight && } + + { __( 'Request MJML API keys', 'newspack-newsletters' ) } + +
+
+ +
+ ); +}; diff --git a/src/components/template-modal/screens/template-picker/index.js b/src/components/template-modal/screens/template-picker/index.js new file mode 100644 index 000000000..5d264089d --- /dev/null +++ b/src/components/template-modal/screens/template-picker/index.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { parse } from '@wordpress/blocks'; +import { Fragment, useState } from '@wordpress/element'; +import { Button } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { BlockPreview } from '@wordpress/block-editor'; +import { ENTER, SPACE } from '@wordpress/keycodes'; + +export default ( { onInsertTemplate, templates } ) => { + const [ selectedTemplate, setSelectedTemplate ] = useState( 0 ); + const generateBlockPreview = () => { + return templates && templates[ selectedTemplate ] + ? parse( templates[ selectedTemplate ].content ) + : null; + }; + const blockPreview = generateBlockPreview(); + + return ( + +
+
+
+ { ( templates || [] ).map( ( { title, content }, index ) => ( +
setSelectedTemplate( index ) } + onKeyDown={ event => { + if ( ENTER === event.keyCode || SPACE === event.keyCode ) { + event.preventDefault(); + setSelectedTemplate( index ); + } + } } + role="button" + tabIndex="0" + aria-label={ title } + > +
+ +
+
{ title }
+
+ ) ) } +
+
+ +
+ { blockPreview && blockPreview.length > 0 ? ( + + ) : ( +

{ __( 'Select a layout to preview.', 'newspack-newsletters' ) }

+ ) } +
+
+ { selectedTemplate !== null && ( + + ) } +
+ ); +}; diff --git a/src/components/template-modal/style.scss b/src/components/template-modal/style.scss index 65c3951ad..a991831e9 100644 --- a/src/components/template-modal/style.scss +++ b/src/components/template-modal/style.scss @@ -62,6 +62,9 @@ top: 12px; z-index: 11; } + .error { + color: red; + } } &__patterns { diff --git a/src/editor/index.js b/src/editor/index.js index 654211baa..96adebf4c 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -59,8 +59,10 @@ const NewsletterEdit = ( { const templates = window && window.newspack_newsletters_data && window.newspack_newsletters_data.templates; - const [ selectedTemplate, setSelectedTemplate ] = useState( 0 ); const [ insertedTemplate, setInserted ] = useState( null ); + const [ hasKeys, setHasKeys ] = useState( + window && window.newspack_newsletters_data && window.newspack_newsletters_data.has_keys + ); const handleTemplateInsertion = templateIndex => { const template = templates[ templateIndex ]; @@ -73,16 +75,16 @@ const NewsletterEdit = ( { setInserted( templateIndex ); setTimeout( savePost, 1 ); }; - const isDisplayingTemplateModal = - isEditedPostEmpty && templates && templates.length && insertedTemplate === null; + ! hasKeys || + ( isEditedPostEmpty && templates && templates.length && insertedTemplate === null ); return isDisplayingTemplateModal ? ( setHasKeys( status ) } /> ) : (