From 4a9f4423544302bd54068723c79ab02f6ac663cc Mon Sep 17 00:00:00 2001 From: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com> Date: Mon, 1 Nov 2021 13:50:08 -0600 Subject: [PATCH] Creator Onboarding: Creator Setup View (#14728) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com> Co-authored-by: Yhëhtozr Co-authored-by: yheuhtozr <84892012+yheuhtozr@users.noreply.github.com> Co-authored-by: Nick Taylor --- .../initializers/initializeBodyData.js | 5 ++ .../admin/creator_settings_controller.rb | 38 ++++++++++ app/controllers/async_info_controller.rb | 12 ++- .../packs/onboardingRedirectCheck.jsx | 22 +++++- app/models/creator_setting.rb | 9 +++ .../admin/creator_settings/_form.html.erb | 73 +++++++++++++++++++ app/views/admin/creator_settings/new.html.erb | 48 ++++++++++++ app/views/admin/users/edit.html.erb | 1 - .../podcast_episodes/_episodes_feed.html.erb | 2 +- config/locales/views/misc/en.yml | 2 +- config/locales/views/misc/fr.yml | 2 +- config/routes/admin.rb | 4 +- .../creatorSettings.spec.js | 71 ++++++++++++++++++ .../creatorSignup.spec.js | 5 +- cypress/support/commands.js | 2 +- spec/requests/admin/creator_settings_spec.rb | 62 ++++++++++++++++ spec/requests/async_info_spec.rb | 2 +- 17 files changed, 349 insertions(+), 11 deletions(-) create mode 100644 app/controllers/admin/creator_settings_controller.rb create mode 100644 app/models/creator_setting.rb create mode 100644 app/views/admin/creator_settings/_form.html.erb create mode 100644 app/views/admin/creator_settings/new.html.erb create mode 100644 cypress/integration/creatorOnboardingFlows/creatorSettings.spec.js create mode 100644 spec/requests/admin/creator_settings_spec.rb diff --git a/app/assets/javascripts/initializers/initializeBodyData.js b/app/assets/javascripts/initializers/initializeBodyData.js index 01d20f953..2edd5f748 100644 --- a/app/assets/javascripts/initializers/initializeBodyData.js +++ b/app/assets/javascripts/initializers/initializeBodyData.js @@ -41,7 +41,10 @@ function fetchBaseData() { // Assigning User if (checkUserLoggedIn()) { document.body.dataset.user = json.user; + document.body.dataset.creator = json.creator; + document.body.dataset.creatorOnboarding = json.creator_onboarding; browserStoreCache('set', json.user); + setTimeout(() => { if (typeof ga === 'function') { ga('set', 'userId', JSON.parse(json.user).id); @@ -50,6 +53,8 @@ function fetchBaseData() { } else { // Ensure user data is not exposed if no one is logged in delete document.body.dataset.user; + delete document.body.dataset.creator; + delete document.body.dataset.creatorOnboarding; browserStoreCache('remove'); } } diff --git a/app/controllers/admin/creator_settings_controller.rb b/app/controllers/admin/creator_settings_controller.rb new file mode 100644 index 000000000..93955e66e --- /dev/null +++ b/app/controllers/admin/creator_settings_controller.rb @@ -0,0 +1,38 @@ +module Admin + class CreatorSettingsController < Admin::ApplicationController + ALLOWED_PARAMS = %i[community_name logo_svg primary_brand_color_hex invite_only_mode public].freeze + + def new; end + + def create + extra_authorization + ActiveRecord::Base.transaction do + ::Settings::Community.community_name = settings_params[:community_name] + ::Settings::General.logo_svg = settings_params[:logo_svg] + ::Settings::UserExperience.primary_brand_color_hex = settings_params[:primary_brand_color_hex] + ::Settings::Authentication.invite_only_mode = settings_params[:invite_only] + ::Settings::UserExperience.public = settings_params[:public] + end + # For this feature to work as expected for the time being, we must set the COC and TOS to true. + # However, this is not a viable solution, as Forem Creators are required to see and check the + # COC and TOS. Bypassing them in this manner will not do and we will need to rethink this solution. + # TODO: Replace the current solution of setting the COC and TOS to true with a better, more + # long-term solution for Forem Creators. + current_user.update!(saw_onboarding: true, checked_code_of_conduct: true, checked_terms_and_conditions: true) + redirect_to root_path + rescue StandardError => e + flash.now[:error] = e.message + render new_admin_creator_setting_path + end + + private + + def extra_authorization + not_authorized unless current_user.has_role?(:creator) + end + + def settings_params + params.permit(ALLOWED_PARAMS) + end + end +end diff --git a/app/controllers/async_info_controller.rb b/app/controllers/async_info_controller.rb index b1331e4d7..e96a5dc01 100644 --- a/app/controllers/async_info_controller.rb +++ b/app/controllers/async_info_controller.rb @@ -18,7 +18,9 @@ class AsyncInfoController < ApplicationController broadcast: broadcast_data, param: request_forgery_protection_token, token: form_authenticity_token, - user: user_data + user: user_data, + creator: user_is_a_creator, + creator_onboarding: use_creator_onboarding } end end @@ -62,6 +64,14 @@ class AsyncInfoController < ApplicationController end.to_json end + def user_is_a_creator + @user.creator? + end + + def use_creator_onboarding + FeatureFlag.enabled?(:creator_onboarding) && user_is_a_creator + end + def user_cache_key "user-info-#{current_user&.id}__ #{current_user&.last_sign_in_at}__ diff --git a/app/javascript/packs/onboardingRedirectCheck.jsx b/app/javascript/packs/onboardingRedirectCheck.jsx index 1158841ea..c42e079ad 100644 --- a/app/javascript/packs/onboardingRedirectCheck.jsx +++ b/app/javascript/packs/onboardingRedirectCheck.jsx @@ -9,11 +9,13 @@ HTMLDocument.prototype.ready = new Promise((resolve) => { return null; }); +// If localStorage.getItem('shouldRedirectToOnboarding') is not set, i.e. null, that means we should redirect. function redirectableLocation() { return ( window.location.pathname !== '/onboarding' && window.location.pathname !== '/signout_confirm' && - window.location.pathname !== '/privacy' + window.location.pathname !== '/privacy' && + window.location.pathname !== '/admin/creator_settings/new' ); } @@ -25,6 +27,14 @@ function onboardingSkippable(currentUser) { ); } +function onboardCreator(currentUser) { + return ( + document.body.dataset.creator === 'true' && + document.body.dataset.creatorOnboarding === 'true' && + !currentUser.saw_onboarding + ); +} + document.ready.then( getUserDataAndCsrfToken() .then(({ currentUser, csrfToken }) => { @@ -32,7 +42,9 @@ document.ready.then( window.csrfToken = csrfToken; getUnopenedChannels(); - if (redirectableLocation() && !onboardingSkippable(currentUser)) { + if (redirectableLocation() && onboardCreator(currentUser)) { + window.location = `${window.location.origin}/admin/creator_settings/new?referrer=${window.location}`; + } else if (redirectableLocation() && !onboardingSkippable(currentUser)) { window.location = `${window.location.origin}/onboarding?referrer=${window.location}`; } }) @@ -46,6 +58,12 @@ window.InstantClick.on('change', () => { getUserDataAndCsrfToken() .then(({ currentUser }) => { if ( + redirectableLocation() && + localStorage.getItem('shouldRedirectToOnboarding') === null && + onboardCreator(currentUser) + ) { + window.location = `${window.location.origin}/admin/creator_settings/new?referrer=${window.location}`; + } else if ( redirectableLocation() && localStorage.getItem('shouldRedirectToOnboarding') === null && !onboardingSkippable(currentUser) diff --git a/app/models/creator_setting.rb b/app/models/creator_setting.rb new file mode 100644 index 000000000..63bc61427 --- /dev/null +++ b/app/models/creator_setting.rb @@ -0,0 +1,9 @@ +class CreatorSetting < ApplicationRecord + # This class exists to take advantage of Rolify for limiting authorization + # on internal reports. + # NOTE: It is not backed by a database table and should not be expected to + # function like a traditional Rails model + self.abstract_class = true + + resourcify +end diff --git a/app/views/admin/creator_settings/_form.html.erb b/app/views/admin/creator_settings/_form.html.erb new file mode 100644 index 000000000..9f5bba462 --- /dev/null +++ b/app/views/admin/creator_settings/_form.html.erb @@ -0,0 +1,73 @@ +
+ <%= label_tag :community_name, class: "crayons-field__label" do %> + Community name + +

Used as the primary name for your Forem.

+ <% end %> + <%= text_field_tag :community_name, "", placeholder: "Climbing Life", class: "crayons-textfield fs-italic", required: true %> +
+ +
+ <%= label_tag :logo_svg, class: "crayons-field__label" do %> + Logo + +

Ideally SVG, but PNG will work, too. Min size: 300x300px.

+ <% end %> + + <%= file_field_tag :logo_svg, class: "crayons-btn crayons-btn--secondary", role: "button", required: true %> + + + <% if ::Settings::General.logo_svg.present? %> + + <% end %> +
+ +
+ <%= label_tag :primary_brand_color_hex, class: "crayons-field__label" do %> + Brand color + +

This will be the "accent" color used for buttons, links, etc.

+ <% end %> + +
+
+ <%= text_field_tag :primary_brand_color_hex, + ::Settings::UserExperience.primary_brand_color_hex, + pattern: "^#+([a-fA-F0-9]{6})$", + placeholder: ::Settings::UserExperience.primary_brand_color_hex, + class: "crayons-textfield js-color-field" %> + <%= color_field_tag :primary_brand_color_hex, + ::Settings::UserExperience.primary_brand_color_hex, + pattern: "^#+([a-fA-F0-9]{6})$", + placeholder: ::Settings::UserExperience.primary_brand_color_hex, + class: "crayons-color-selector js-color-field ml-2", + required: true %> +
+
+ +
+
+ Who can join this community? +
+ <%= radio_button_tag :invite_only_mode, "0", class: "crayons-field crayons-field--radio", required: true %> + +
+ <%= radio_button_tag :invite_only_mode, "1", class: "crayons-field crayons-field--radio", required: true %> + +
+
+ +
+
+ Who can view content in this community? +
+ <%= radio_button_tag :public, "0", class: "crayons-field crayons-field--radio", required: true %> + +
+ <%= radio_button_tag :public, "1", class: "crayons-field crayons-field--radio", required: true %> + +
+
+
diff --git a/app/views/admin/creator_settings/new.html.erb b/app/views/admin/creator_settings/new.html.erb new file mode 100644 index 000000000..9aeb0e4b1 --- /dev/null +++ b/app/views/admin/creator_settings/new.html.erb @@ -0,0 +1,48 @@ +<% if FeatureFlag.enabled?(:creator_onboarding) && User.with_role(:creator).any? %> + +
+
+ <% if flash[:error] %> + + <% end %> +
+ + <%= form_tag(admin_creator_settings_path, method: "post") do %> + <% if defined?(resource) && resource&.errors&.any? %> + + <% end %> + +
+

Lovely! Let's set up your Forem.

+

No stress, you can always change it later.

+
+
+ <%= render "form" %> +
+ <%= submit_tag "Finish", class: "crayons-btn btn--primary" %> +
+ <% end %> +<% end %> + +<%= inline_svg_tag("forem-background.svg", aria_hidden: true, class: "forem-background absolute bottom-0 right-0") %> diff --git a/app/views/admin/users/edit.html.erb b/app/views/admin/users/edit.html.erb index c85a360b1..f6d58a0e1 100644 --- a/app/views/admin/users/edit.html.erb +++ b/app/views/admin/users/edit.html.erb @@ -105,7 +105,6 @@
-

Destructive Actions

diff --git a/app/views/podcast_episodes/_episodes_feed.html.erb b/app/views/podcast_episodes/_episodes_feed.html.erb index 011589004..7e5a1c98a 100644 --- a/app/views/podcast_episodes/_episodes_feed.html.erb +++ b/app/views/podcast_episodes/_episodes_feed.html.erb @@ -4,7 +4,7 @@
<%= optimized_image_tag(episode.image_url || episode.podcast.image_url, optimizer_options: { crop: "imagga_scale", width: 240, height: 240 }, - image_options: { alt: episode.title, loading: "lazy"}) %> + image_options: { alt: episode.title, loading: "lazy" }) %>

<%= t("views.podcasts.tag") %><%= episode.title %>

diff --git a/config/locales/views/misc/en.yml b/config/locales/views/misc/en.yml index eeb992a55..c466a5ede 100644 --- a/config/locales/views/misc/en.yml +++ b/config/locales/views/misc/en.yml @@ -86,4 +86,4 @@ en: description: All videos on %{community} heading: "%{community} on Video" beta_html: "Video is beta: %{contact}" - upload: "🎥 Upload Video File 🙌" + upload: "🎥 Upload Video File 🙌" \ No newline at end of file diff --git a/config/locales/views/misc/fr.yml b/config/locales/views/misc/fr.yml index 7d4c802a6..c3f4fe08e 100644 --- a/config/locales/views/misc/fr.yml +++ b/config/locales/views/misc/fr.yml @@ -86,4 +86,4 @@ fr: description: All videos on %{community} heading: "%{community} en Vidéo" beta_html: "Vidéo est en bêta: %{contact}" - upload: "🎥 Télécharger un fichier vidéo 🙌" + upload: "🎥 Télécharger un fichier vidéo 🙌" \ No newline at end of file diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 6ec0b1a9e..d3a67ab6b 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -13,7 +13,9 @@ namespace :admin do resources :invitations, only: %i[index new create destroy] resources :organization_memberships, only: %i[update destroy create] resources :permissions, only: %i[index] - resources :reactions, only: [:update] + resources :reactions, only: %i[update] + resources :creator_settings, only: %i[create new] + namespace :settings do resources :authentications, only: [:create] resources :campaigns, only: [:create] diff --git a/cypress/integration/creatorOnboardingFlows/creatorSettings.spec.js b/cypress/integration/creatorOnboardingFlows/creatorSettings.spec.js new file mode 100644 index 000000000..164ae4388 --- /dev/null +++ b/cypress/integration/creatorOnboardingFlows/creatorSettings.spec.js @@ -0,0 +1,71 @@ +describe('Creator Setup Page', () => { + const { baseUrl } = Cypress.config(); + + beforeEach(() => { + cy.testSetup(); + cy.fixture('users/creatorUser.json').as('creator'); + cy.get('@creator').then((creator) => { + cy.loginCreator(creator); + }); + + cy.visit(`${baseUrl}admin/creator_settings/new?referrer=${baseUrl}`); + }); + + it('should submit the creator settings form', () => { + // should display a welcome message + cy.findByText("Lovely! Let's set up your Forem.").should('be.visible'); + cy.findByText('No stress, you can always change it later.').should( + 'be.visible', + ); + + // should contain a community name and update the field properly + cy.findByRole('textbox', { name: /community name/i }) + .as('communityName') + .invoke('attr', 'placeholder') + .should('eq', 'Climbing Life'); + cy.get('@communityName').type('Climbing Life'); + + // should contain a logo upload field and upload a logo upon click + cy.findByText(/^Logo/).should('be.visible'); + cy.findByRole('button', { name: /logo/i }).attachFile( + '/images/admin-image.png', + ); + + // should contain a brand color field + cy.findByText(/^Brand color/).should('be.visible'); + cy.findByText(/^Brand color/).invoke('attr', 'value', '#ff0000'); + + // should contain a 'Who can join this community?' radio selector field and allow selection upon click + cy.findByRole('group', { name: /^Who can join this community/i }).should( + 'be.visible', + ); + cy.findAllByRole('radio', { name: /everyone/i }).check(); + cy.findAllByRole('radio').should('be.checked'); + + // should contain a 'Who can view content in this community?' radio selector field and allow selection upon click + cy.findByRole('group', { + name: /^Who can view content in this community/i, + }).should('be.visible'); + cy.findAllByRole('radio', { name: /members only/i }).check(); + cy.findAllByRole('radio').should('be.checked'); + + // should redirect the creator to the home page when the form is completely filled out and 'Finish' is clicked + cy.findByRole('button', { name: 'Finish' }).click(); + cy.url().should('equal', baseUrl); + }); + + it('should not submit the creator settings form if any of the fields are not filled out', () => { + // TODO: Circle back around to testing this once the styling for the form is complete + cy.findByRole('textbox', { name: /community name/i }).should( + 'have.attr', + 'required', + ); + cy.findByRole('button', { name: /logo/i }).should('have.attr', 'required'); + // should not redirect the creator to the home page when the form is not completely filled out and 'Finish' is clicked + cy.findByRole('button', { name: 'Finish' }).click(); + cy.url().should( + 'equal', + `${baseUrl}admin/creator_settings/new?referrer=${baseUrl}`, + ); + }); +}); diff --git a/cypress/integration/creatorOnboardingFlows/creatorSignup.spec.js b/cypress/integration/creatorOnboardingFlows/creatorSignup.spec.js index 14609bc79..f45e58288 100644 --- a/cypress/integration/creatorOnboardingFlows/creatorSignup.spec.js +++ b/cypress/integration/creatorOnboardingFlows/creatorSignup.spec.js @@ -96,6 +96,9 @@ describe('Creator Signup Page', () => { .click(); const { baseUrl } = Cypress.config(); - cy.url().should('equal', `${baseUrl}onboarding?referrer=${baseUrl}`); + cy.url().should( + 'equal', + `${baseUrl}admin/creator_settings/new?referrer=${baseUrl}`, + ); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index f867ff828..f55a26cf8 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -138,7 +138,7 @@ Cypress.Commands.add('loginCreator', ({ name, username, email, password }) => { return cy.request( 'POST', '/users', - `utf8=%E2%9C%93&user%5Bname%5D=${encodedName}&user%5Busername%5D=${encodedUsername}&user%5Bemail%5D=${encodedEmail}%40forem.local&user%5Bpassword%5D=${encodedPassword}&commit=Create+my+account`, + `utf8=%E2%9C%93&user%5Bname%5D=${encodedName}&user%5Busername%5D=${encodedUsername}&user%5Bemail%5D=${encodedEmail}%40forem.local&user%5Bpassword%5D=${encodedPassword}&commit=Create+my+account&user%5Bforem_owner_secret%5D=secret`, ); } diff --git a/spec/requests/admin/creator_settings_spec.rb b/spec/requests/admin/creator_settings_spec.rb new file mode 100644 index 000000000..4e8dd3f06 --- /dev/null +++ b/spec/requests/admin/creator_settings_spec.rb @@ -0,0 +1,62 @@ +require "rails_helper" + +RSpec.describe "/creator_settings/new", type: :request do + let!(:creator) { create(:user, :creator) } + let!(:non_admin_user) { create(:user) } + let(:params) do + { community_name: "Climbing Life", + logo_svg: "https://dummyimage.com/300x300.png", + primary_brand_color_hex: "000000", + public: true, + invite_only: false } + end + + before do + allow(FeatureFlag).to receive(:enabled?).with(:creator_onboarding).and_return(true) + allow(Settings::General).to receive(:waiting_on_first_user).and_return(false) + end + + describe "GET /admin/creator_settings/new" do + before do + sign_in creator + get new_admin_creator_setting_path + end + + context "when the user is a creator" do + it "allows the request" do + expect(response).to have_http_status(:ok) + end + + it "renders the correct page" do + expect(response.body).to include("Lovely! Let's set up your Forem.") + end + end + + context "when the user is a not a creator" do + before do + sign_in non_admin_user + end + + it "blocks the request" do + expect do + get new_admin_creator_setting_path + end.to raise_error(Pundit::NotAuthorizedError) + end + end + + describe "POST /admin/creator_settings/new" do + before do + sign_in creator + get new_admin_creator_setting_path + end + + it "allows a creator to successfully fill out the creator setup form", :aggregate_failures do + post admin_creator_settings_path, params: params + expect(creator.saw_onboarding).to eq(true) + expect(creator.checked_code_of_conduct).to eq(true) + expect(creator.checked_terms_and_conditions).to eq(true) + expect(response).to have_http_status(:ok) + end + end + end +end diff --git a/spec/requests/async_info_spec.rb b/spec/requests/async_info_spec.rb index 63e593f8e..126b38b9d 100644 --- a/spec/requests/async_info_spec.rb +++ b/spec/requests/async_info_spec.rb @@ -26,7 +26,7 @@ RSpec.describe "AsyncInfo", type: :request do sign_in create(:user) get "/async_info/base_data" - expect(response.parsed_body.keys).to match_array(%w[broadcast param token user]) + expect(response.parsed_body.keys).to match_array(%w[broadcast creator creator_onboarding param token user]) end end end