diff --git a/app/assets/stylesheets/onboarding.scss b/app/assets/stylesheets/onboarding.scss index 748dc495e..b3ad31107 100644 --- a/app/assets/stylesheets/onboarding.scss +++ b/app/assets/stylesheets/onboarding.scss @@ -73,3 +73,32 @@ right: var(--su-6); } } + +.onboarding-main { + .email-preferences-wrapper { + h1 { + margin-top: 1.5rem; + margin-bottom: 0.5rem; + line-height: var(--lh-tight); + } + p { + line-height: var(--lh-base); + } + } + + .onboarding_newsletter_opt_in_head { + font-weight: 600; + } + + .onboarding_newsletter_opt_in_subhead { + color: var(--base-60); + } + + .email_newsletter { + border: 1px solid var(--card-border); + border-radius: 0 6px 6px 0; + border-left-width: thick; + border-left-color: var(--accent-brand); + background-color: var(--card-secondary-bg); + } +} diff --git a/app/controllers/onboardings_controller.rb b/app/controllers/onboardings_controller.rb index 51cb468af..8878501e5 100644 --- a/app/controllers/onboardings_controller.rb +++ b/app/controllers/onboardings_controller.rb @@ -80,6 +80,17 @@ class OnboardingsController < ApplicationController notifications_updated_response(success, current_user.notification_setting.errors_as_sentence) end + def newsletter + respond_to do |format| + format.json do + rendered_content = render_to_string(partial: "onboardings/newsletter", + formats: [:html], + layout: false) + render json: { content: rendered_content } + end + end + end + private def unset_username? diff --git a/app/javascript/onboarding/components/EmailPreferencesForm.jsx b/app/javascript/onboarding/components/EmailPreferencesForm.jsx index 8371a2977..2c35542e0 100644 --- a/app/javascript/onboarding/components/EmailPreferencesForm.jsx +++ b/app/javascript/onboarding/components/EmailPreferencesForm.jsx @@ -8,15 +8,19 @@ export class EmailPreferencesForm extends Component { constructor(props) { super(props); - this.handleChange = this.handleChange.bind(this); this.onSubmit = this.onSubmit.bind(this); - this.state = { - email_newsletter: false, + content: '

Loading...

' }; } componentDidMount() { + fetch('/onboarding/newsletter') + .then((response) => response.json()) + .then((json) => { + this.setState({ content: json['content'] }); + }); + updateOnboarding('v2: email preferences form'); } @@ -40,15 +44,7 @@ export class EmailPreferencesForm extends Component { }); } - handleChange(event) { - const { name } = event.target; - this.setState((currentState) => ({ - [name]: !currentState[name], - })); - } - render() { - const { email_newsletter } = this.state; const { prev, slidesCount, currentSlideIndex } = this.props; return (
-
-
-

- Almost there! -

-

- Review your email preferences before we continue. -

-
+
-
-
- Email preferences -
    -
  • - -
  • -
-
-
-
{ username: 'username', }); + const fakeResponse = JSON.stringify({ + content: ` +

Almost there!

+
+
+
    +
  • + +
  • +
+
+
+ `, + }); + + beforeEach(() => { + fetch.resetMocks(); + fetch.mockResponseOnce(fakeResponse); + }); + beforeAll(() => { document.head.innerHTML = ''; @@ -46,20 +66,16 @@ describe('EmailPreferencesForm', () => { expect(results).toHaveNoViolations(); }); - it('should load the appropriate text', () => { - const { queryByText } = renderEmailPreferencesForm(); - - expect(queryByText(/almost there!/i)).toExist(); - expect( - queryByText(/review your email preferences before we continue./i), - ).toExist(); - expect(queryByText('Email preferences')).toExist(); + it('should load the appropriate text', async () => { + const { findByLabelText } = renderEmailPreferencesForm(); + await findByLabelText(/receive weekly newsletter/i); + expect(document.body.innerHTML).toMatchSnapshot(); }); - it('should show the checkbox unchecked', () => { - const { queryByLabelText } = renderEmailPreferencesForm(); - - expect(queryByLabelText(/receive weekly newsletter/i).checked).toBe(false); + it('should show the checkbox unchecked', async () => { + const { findByLabelText } = renderEmailPreferencesForm(); + const checkbox = await findByLabelText(/receive weekly newsletter/i); + expect(checkbox.checked).toBe(false); }); it('should render a stepper', () => { diff --git a/app/javascript/onboarding/components/__tests__/__snapshots__/EmailPreferencesForm.test.jsx.snap b/app/javascript/onboarding/components/__tests__/__snapshots__/EmailPreferencesForm.test.jsx.snap new file mode 100644 index 000000000..96389e4c8 --- /dev/null +++ b/app/javascript/onboarding/components/__tests__/__snapshots__/EmailPreferencesForm.test.jsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmailPreferencesForm should load the appropriate text 1`] = ` +"
+

Almost there!

+
+
+
    +
  • + +
  • +
+
+
+
" +`; diff --git a/app/lib/constants/settings/general.rb b/app/lib/constants/settings/general.rb index 534897b1b..da186ab4e 100644 --- a/app/lib/constants/settings/general.rb +++ b/app/lib/constants/settings/general.rb @@ -89,6 +89,18 @@ module Constants description: "", placeholder: I18n.t("lib.constants.settings.general.meta_keywords.description") }, + onboarding_newsletter_content: { + description: I18n.t("lib.constants.settings.general.onboarding_newsletter_content.description"), + placeholder: I18n.t("lib.constants.settings.general.onboarding_newsletter_content.placeholder") + }, + onboarding_newsletter_opt_in_head: { + description: I18n.t("lib.constants.settings.general.onboarding_newsletter_opt_in_head.description"), + placeholder: I18n.t("lib.constants.settings.general.onboarding_newsletter_opt_in_head.placeholder") + }, + onboarding_newsletter_opt_in_subhead: { + description: I18n.t("lib.constants.settings.general.onboarding_newsletter_opt_in_subhead.description"), + placeholder: I18n.t("lib.constants.settings.general.onboarding_newsletter_opt_in_subhead.placeholder") + }, payment_pointer: { description: I18n.t("lib.constants.settings.general.payment.description"), placeholder: "$pay.somethinglikethis.co/value" diff --git a/app/models/settings/base.rb b/app/models/settings/base.rb index c2699f369..398b60138 100644 --- a/app/models/settings/base.rb +++ b/app/models/settings/base.rb @@ -73,7 +73,9 @@ module Settings if result.nil? # we don't want to accidentally do this for "false" result ||= default.is_a?(Proc) ? default.call : default end - result = __send__(:convert_string_to_value_type, type, result, separator: separator) + + read_as_type = type == :markdown ? :string : type + result = __send__(:convert_string_to_value_type, read_as_type, result, separator: separator) result end @@ -83,10 +85,17 @@ module Settings var_name = key record = find_by(var: var_name) || new(var: var_name) - value = __send__(:convert_string_to_value_type, type, value, separator: separator) - record.value = value - record.save! + if type == :markdown + processed = __send__(:convert_string_to_value_type, type, value) + record.value = value + record.save! + __send__(:"#{key}_processed_html=", processed) + else + value = __send__(:convert_string_to_value_type, type, value, separator: separator) + record.value = value + record.save! + end value end @@ -127,6 +136,8 @@ module Settings value.to_f when :big_decimal value.to_d + when :markdown + ContentRenderer.new(value).process.processed_html else value end diff --git a/app/models/settings/general.rb b/app/models/settings/general.rb index 4df6c9eb2..17715f8f7 100644 --- a/app/models/settings/general.rb +++ b/app/models/settings/general.rb @@ -123,9 +123,21 @@ module Settings existing_published_article_id: true, allow_nil: true } + # Onboarding newsletter + setting :onboarding_newsletter_content, type: :markdown + setting :onboarding_newsletter_content_processed_html + setting :onboarding_newsletter_opt_in_head + setting :onboarding_newsletter_opt_in_subhead + setting :default_content_language, type: :string, default: "en", validates: { inclusion: Languages::Detection.codes } + def self.custom_newsletter_configured? + onboarding_newsletter_content_processed_html.present? && + onboarding_newsletter_opt_in_head.present? && + onboarding_newsletter_opt_in_subhead.present? + end + def self.social_media_services SOCIAL_MEDIA_SERVICES.index_with do |name| social_media_handles[name] diff --git a/app/services/content_renderer.rb b/app/services/content_renderer.rb index 6efcedba0..25b82d2c3 100644 --- a/app/services/content_renderer.rb +++ b/app/services/content_renderer.rb @@ -1,4 +1,4 @@ -# renders markdown for Articles, Billboards, Comments +# renders markdown for Articles, Billboards, Comments, Onboarding newsletter content class ContentRenderer Result = Struct.new(:front_matter, :reading_time, :processed_html, keyword_init: true) @@ -9,10 +9,10 @@ class ContentRenderer end # @param input [String] body_markdown to process - # @param source [Article, Comment, Billboard] + # @param source [optional, possibly Article, Comment, Billboard] # @param user [User, NilClass] article's or comment's user, nil for Billboard # @param fixer [Object] fixes the input markdown - def initialize(input, source:, user: nil, fixer: MarkdownProcessor::Fixer::FixAll) + def initialize(input, source: nil, user: nil, fixer: MarkdownProcessor::Fixer::FixAll) @input = input || "" @source = source @user = user diff --git a/app/views/admin/settings/forms/_onboarding.html.erb b/app/views/admin/settings/forms/_onboarding.html.erb index b05973c53..735f0ae8e 100644 --- a/app/views/admin/settings/forms/_onboarding.html.erb +++ b/app/views/admin/settings/forms/_onboarding.html.erb @@ -14,6 +14,30 @@ value: Settings::General.suggested_tags.join(","), placeholder: Constants::Settings::General.details[:suggested_tags][:placeholder] %>
+ +
+ <%= admin_config_label :newsletter_step_heading %> + <%= admin_config_description Constants::Settings::General.details[:onboarding_newsletter_content][:description] %> + <%= f.text_area :onboarding_newsletter_content, + class: "crayons-textfield", + value: Settings::General.onboarding_newsletter_content, + placeholder: Constants::Settings::General.details[:onboarding_newsletter_content][:placeholder] %> +
+ +
+ <%= admin_config_label Constants::Settings::General.details[:onboarding_newsletter_opt_in_head][:description] %> + <%= f.text_field :onboarding_newsletter_opt_in_head, + class: "crayons-textfield", + value: Settings::General.onboarding_newsletter_opt_in_head, + placeholder: Constants::Settings::General.details[:onboarding_newsletter_opt_in_head][:placeholder] %> + + <%= admin_config_label Constants::Settings::General.details[:onboarding_newsletter_opt_in_subhead][:description] %> + <%= f.text_field :onboarding_newsletter_opt_in_subhead, + class: "crayons-textfield", + value: Settings::General.onboarding_newsletter_opt_in_subhead, + placeholder: Constants::Settings::General.details[:onboarding_newsletter_opt_in_subhead][:placeholder] %> +
+ <%= render "update_setting_button", f: f %>
diff --git a/app/views/onboardings/_newsletter.html.erb b/app/views/onboardings/_newsletter.html.erb new file mode 100644 index 000000000..d1300c154 --- /dev/null +++ b/app/views/onboardings/_newsletter.html.erb @@ -0,0 +1,63 @@ +<% if feature_flag_enabled?(:onboarding_newsletter_content) %> + <% if Settings::General.custom_newsletter_configured? %> + <%= Settings::General.onboarding_newsletter_content_processed_html.html_safe %> + +
+
+ +
+
+ <% else %> + <%# feature flag is "on" but feature is not configured or only partly configured %> +

Almost there!

+

Review your email preferences before we continue.

+ +
+
+ +
+
+ <% end %> +<% else %> + <%# feature flag is "off", original behavior %> +
+

+ Almost there! +

+

+ Review your email preferences before we continue. +

+
+
+
+ Email preferences + +
+
+<% end %> diff --git a/config/locales/lib/en.yml b/config/locales/lib/en.yml index 08dca1668..c157c5c49 100644 --- a/config/locales/lib/en.yml +++ b/config/locales/lib/en.yml @@ -144,6 +144,15 @@ en: description: The Mascot account meta_keywords: description: 'List of valid keywords: comma separated, letters only e.g. engineering, development' + onboarding_newsletter_content: + description: "Use this field to set the title and subtitle for your newsletter offering step. Keep it concise." + placeholder: "You can configure image, title and subtitle via markdown" + onboarding_newsletter_opt_in_head: + description: "Newsletter checkbox heading" + placeholder: "E.g., Yes, subscribe me to the weekly newsletter" + onboarding_newsletter_opt_in_subhead: + description: "Newsletter checkbox subheading" + placeholder: "E.g., Rest assured, we respect your privacy. Your email will only be used for our newsletter and won't be shared with others." payment: description: 'Used for site-wide web monetization. See: https://github.com/forem/forem/pull/6345' periodic: diff --git a/config/locales/lib/fr.yml b/config/locales/lib/fr.yml index b43fc64ed..67ab72066 100644 --- a/config/locales/lib/fr.yml +++ b/config/locales/lib/fr.yml @@ -144,6 +144,15 @@ fr: description: The Mascot account meta_keywords: description: 'List of valid keywords: comma separated, letters only e.g. engineering, development' + onboarding_newsletter_content: + description: "Use this field to set the title and subtitle for your newsletter offering step. Keep it concise." + placeholder: "You can configure image, title and subtitle via markdown" + onboarding_newsletter_opt_in_head: + description: "Newsletter checkbox heading" + placeholder: "E.g., Yes, subscribe me to the weekly newsletter" + onboarding_newsletter_opt_in_subhead: + description: "Newsletter checkbox subheading" + placeholder: "E.g., Rest assured, we respect your privacy. Your email will only be used for our newsletter and won't be shared with others." payment: description: 'Used for site-wide web monetization. See: https://github.com/forem/forem/pull/6345' periodic: diff --git a/config/routes.rb b/config/routes.rb index a33d7bf3e..29761617b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -178,6 +178,7 @@ Rails.application.routes.draw do patch :notifications, defaults: { format: :json } get :tags, defaults: { format: :json } get :users_and_organizations, defaults: { format: :json } + get :newsletter, defaults: { format: :json } end end diff --git a/spec/models/settings/base_spec.rb b/spec/models/settings/base_spec.rb index ca56bfc9a..c449a759a 100644 --- a/spec/models/settings/base_spec.rb +++ b/spec/models/settings/base_spec.rb @@ -30,6 +30,8 @@ RSpec.describe Settings::Base do setting :float_item, type: :float, default: 7 setting :big_decimal_item, type: :big_decimal, default: 9 setting :default_value_with_block, type: :integer, default: -> { 1 + 1 } + setting :some_markdown, type: :markdown + setting :some_markdown_processed_html end end @@ -49,7 +51,7 @@ RSpec.describe Settings::Base do describe ".keys" do it "returns all the defined settings", :aggregate_failures do - expect(TestSetting.keys.size).to eq 10 + expect(TestSetting.keys.size).to eq 12 expect(TestSetting.keys).to include("host") expect(TestSetting.keys).to include("default_value_with_block") end @@ -132,6 +134,19 @@ RSpec.describe Settings::Base do TestSetting.big_decimal_item = 5 expect(TestSetting.big_decimal_item).to be_an_instance_of(BigDecimal) end + + it "can be coerced from markdown to parsed_html" do + some_markdown = <<~HEREDOC + Hi, Hello! + + This is **markdown**. + HEREDOC + TestSetting.some_markdown = some_markdown + + processed_html = "

Hi, Hello!

\n\n

This is markdown.

\n\n" + expect(TestSetting.some_markdown_processed_html).to eq(processed_html) + expect(TestSetting.some_markdown).to eq(some_markdown) + end end describe "validations" do