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.
-
-
+
-
-
{
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`] = `
+""
+`;
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 %>
+
+
+<% 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\nThis 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