diff --git a/app/assets/images/eye-off.svg b/app/assets/images/eye-off.svg new file mode 100644 index 000000000..580d5e028 --- /dev/null +++ b/app/assets/images/eye-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/eye.svg b/app/assets/images/eye.svg new file mode 100644 index 000000000..8a8728a3c --- /dev/null +++ b/app/assets/images/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/pencil.svg b/app/assets/images/pencil.svg new file mode 100644 index 000000000..deced8f6e --- /dev/null +++ b/app/assets/images/pencil.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/twemoji/hands.svg b/app/assets/images/twemoji/hands.svg index a93f03c1d..9c4dc90ce 100644 --- a/app/assets/images/twemoji/hands.svg +++ b/app/assets/images/twemoji/hands.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/app/assets/images/twemoji/suspicious.svg b/app/assets/images/twemoji/suspicious.svg index 1a58278bd..fe2d4c588 100644 --- a/app/assets/images/twemoji/suspicious.svg +++ b/app/assets/images/twemoji/suspicious.svg @@ -1,17 +1,17 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/app/assets/images/twemoji/thinking.svg b/app/assets/images/twemoji/thinking.svg index 7f0cf6403..54b9d0f30 100644 --- a/app/assets/images/twemoji/thinking.svg +++ b/app/assets/images/twemoji/thinking.svg @@ -1,8 +1,6 @@ - - - - - - + + + + diff --git a/app/assets/images/twemoji/thumb-down.svg b/app/assets/images/twemoji/thumb-down.svg index b86f274f2..603c58fa6 100644 --- a/app/assets/images/twemoji/thumb-down.svg +++ b/app/assets/images/twemoji/thumb-down.svg @@ -1,4 +1,4 @@ - - + + diff --git a/app/assets/images/twemoji/thumb-up.svg b/app/assets/images/twemoji/thumb-up.svg index f593c6f76..3d0d8d9c1 100644 --- a/app/assets/images/twemoji/thumb-up.svg +++ b/app/assets/images/twemoji/thumb-up.svg @@ -1,4 +1,4 @@ - - + + diff --git a/app/assets/stylesheets/views/confirm-email.scss b/app/assets/stylesheets/views/confirm-email.scss index 540d38134..6da1921bf 100644 --- a/app/assets/stylesheets/views/confirm-email.scss +++ b/app/assets/stylesheets/views/confirm-email.scss @@ -17,10 +17,10 @@ padding: 1em 1em 0; } } +} - .forem-background { - @media (max-width: $breakpoint-m) { - display: none; - } +.forem-background { + @media (max-width: $breakpoint-l) { + display: none; } } diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 6a2540128..8347365e9 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -70,6 +70,9 @@ class RegistrationsController < Devise::RegistrationsController resource.registered_at = Time.current resource.build_setting(editor_version: "v2") resource.remote_profile_image_url = Users::ProfileImageGenerator.call if resource.remote_profile_image_url.blank? + if FeatureFlag.enabled?(:creator_onboarding) + resource.password_confirmation = resource.password + end check_allowed_email(resource) if resource.email.present? resource.save if resource.email.present? end diff --git a/app/javascript/packs/foremCreatorSignup.js b/app/javascript/packs/foremCreatorSignup.js new file mode 100644 index 000000000..d6dfff042 --- /dev/null +++ b/app/javascript/packs/foremCreatorSignup.js @@ -0,0 +1,104 @@ +function setDefaultUsername(event) { + if ( + document + .getElementsByClassName('js-creator-signup-username-row')[0] + .classList.contains('hidden') + ) { + const name = event.target.value; + // It's the first user and so we can assume that this username is not taken. + const usernameHint = createUsernameHint(name); + setUsernameHint(usernameHint); + setUsernameField(usernameHint); + showHintRow(); + } +} + +function createUsernameHint(name) { + return name + .toLowerCase() + .replace(/[^a-zA-Z0-9]/g, '_') + .substr(0, 30); +} + +function showHintRow() { + const hintRow = document.getElementsByClassName( + 'js-creator-signup-username-hint-row', + )[0]; + hintRow.classList.remove('hidden'); +} + +function setUsernameHint(usernameHint) { + const usernameHintDisplay = document.getElementsByClassName( + 'js-creator-signup-username-hint', + )[0]; + usernameHintDisplay.innerHTML = usernameHint; +} + +function setUsernameField(usernameHint) { + const usernameField = document.getElementsByClassName( + 'js-creator-signup-username', + )[0]; + usernameField.value = usernameHint; +} + +function showUsernameField() { + const usernameRow = document.getElementsByClassName( + 'js-creator-signup-username-row', + )[0]; + usernameRow.classList.remove('hidden'); + focusUsernameInput(usernameRow); + hideHintRow(); +} + +function focusUsernameInput(usernameRow) { + // A timer with a count of 0 will run when the thread becomes idle + window.setTimeout(() => { + usernameRow.getElementsByTagName('input')[0].focus(); + }, 0); +} + +function hideHintRow() { + const hintRow = document.getElementsByClassName( + 'js-creator-signup-username-hint-row', + )[0]; + hintRow.classList.add('hidden'); +} + +function togglePasswordMask(event) { + event.preventDefault(); + visible = !visible; + toggleAriaPressed(visible); + togglePasswordType(visible); + toggleEyeIcons(visible); +} + +function toggleAriaPressed(visible) { + visibility.setAttribute('aria-pressed', visible); +} + +function togglePasswordType(visible) { + const passwordType = visible ? 'text' : 'password'; + passwordField.type = passwordType; +} + +function toggleEyeIcons(visible) { + eyeOffIcon.classList.toggle('hidden', !visible); + eyeIcon.classList.toggle('hidden', visible); +} + +let visible = false; +const eyeIcon = document.getElementsByClassName('js-eye')[0]; +const eyeOffIcon = document.getElementsByClassName('js-eye-off')[0]; +const passwordField = document.getElementsByClassName('js-password')[0]; +const visibility = document.getElementsByClassName( + 'js-creator-password-visibility', +)[0]; +visibility.addEventListener('click', togglePasswordMask); + +const name = document.getElementsByClassName('js-creator-signup-name')[0]; +name.addEventListener('input', setDefaultUsername); + +const editUsername = document.getElementsByClassName( + 'js-creator-edit-username', +)[0]; +editUsername.addEventListener('click', showUsernameField); diff --git a/app/models/tweet.rb b/app/models/tweet.rb index f625f289f..2966dd5f3 100644 --- a/app/models/tweet.rb +++ b/app/models/tweet.rb @@ -59,7 +59,7 @@ class Tweet < ApplicationRecord status = if status.retweeted_status.present? TwitterClient::Client.status(status.retweeted_status.id.to_s) else - status # rubocop:disable Style/RedundantSelfAssignmentBranch + status end params = { twitter_id_code: status.id.to_s } diff --git a/app/views/devise/registrations/by_email.html.erb b/app/views/devise/registrations/by_email.html.erb index 942d1ac9b..8f83f1610 100644 --- a/app/views/devise/registrations/by_email.html.erb +++ b/app/views/devise/registrations/by_email.html.erb @@ -1 +1,5 @@ +<% if forem_creator_flow_enabled? && waiting_on_first_user? %> + <%= render "shared/authentication/forem_creator_signup" %> +<% else %> <%= render "shared/authentication/email_registration_form" %> +<% end %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index c88438f52..7bef91dc7 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -9,7 +9,7 @@ <% elsif forem_creator_flow_enabled? %> <%= render "shared/authentication/forem_creator_signup" %> <% elsif waiting_on_first_user? %> - <%# TODO: Delete this view once forem creator onboarding is shipped %> + <%# TODO: [@ridhwana]: Delete this view once forem creator onboarding is shipped %> <%= render "shared/authentication/initial_account_wizard" %> <% else %> <%= render "devise/shared/authorization_error" %> diff --git a/app/views/notifications/shared/_comment_box.html.erb b/app/views/notifications/shared/_comment_box.html.erb index 887a7372f..e5101f8e3 100644 --- a/app/views/notifications/shared/_comment_box.html.erb +++ b/app/views/notifications/shared/_comment_box.html.erb @@ -74,7 +74,7 @@ <%= f.hidden_field :commentable_type, value: json_data["comment"]["commentable"]["class"]["name"] %> <%= f.hidden_field :parent_id, value: json_data["comment"]["id"] %> - <%= f.text_area :body_markdown, id: "comment-textarea-for-#{json_data['comment']['id']}", class: "crayons-textfield mb-2 resize-y", placeholder: "Reply...", 'aria-label': "Reply to a comment...", onfocus: "handleFocus(event)" %> + <%= f.text_area :body_markdown, id: "comment-textarea-for-#{json_data['comment']['id']}", class: "crayons-textfield mb-2 resize-y", placeholder: "Reply...", "aria-label": "Reply to a comment...", onfocus: "handleFocus(event)" %>
diff --git a/app/views/shared/authentication/_email_registration_form.html.erb b/app/views/shared/authentication/_email_registration_form.html.erb index be0762581..e64e56f03 100644 --- a/app/views/shared/authentication/_email_registration_form.html.erb +++ b/app/views/shared/authentication/_email_registration_form.html.erb @@ -4,7 +4,9 @@ <%= flash[:notice] %> <% end %> + <%= form_for(@user, as: :user, data: { testid: "registration-form" }, url: registration_path(:user)) do |f| %> + <% if defined?(resource) && resource&.errors&.any? %> - <% elsif forem_creator_flow_enabled? %> - <%# TODO: Extract this into its own form %> -
-

Almost there!

-

Let's create an admin account for your community.

-
<% else %>

Create your account

<% end %> + <% if Settings::Authentication.display_email_domain_allow_list_publicly && Settings::Authentication.allowed_registration_email_domains.any? %>
@@ -46,6 +43,7 @@ <% end %>
<% end %> +
<%= f.label :profile_image, class: "crayons-field__label" %> <%= f.file_field :profile_image, accept: "image/*", class: "crayons-card crayons-card--secondary p-3 flex items-center flex-1 w-100" %> @@ -108,15 +106,8 @@
<% end %> - <% if forem_creator_flow_enabled? %> - <%# TODO: Extract this into its own form %> -
- <%= f.submit "Create admin account", class: "crayons-btn" %> -
- <% else %> -
- <%= f.submit "Sign up", class: "crayons-btn" %> -
- <% end %> +
+ <%= f.submit "Sign up", class: "crayons-btn" %> +
<% end %> diff --git a/app/views/shared/authentication/_forem_creator_signup.html.erb b/app/views/shared/authentication/_forem_creator_signup.html.erb index d513c03c4..5a68ee332 100644 --- a/app/views/shared/authentication/_forem_creator_signup.html.erb +++ b/app/views/shared/authentication/_forem_creator_signup.html.erb @@ -1,3 +1,97 @@ -
- <%= render "shared/authentication/email_registration_form" %> -
+<%= javascript_packs_with_chunks_tag "foremCreatorSignup", defer: true %> +
+
+
+ <% if flash[:notice] %> + + <% end %> + + <% any_errors = local_assigns[:resource]&.errors&.any? %> + + <%= form_for(@user, as: :user, data: { testid: "creator-signup-form" }, url: registration_path(:user)) do |f| %> + <% if any_errors %> + + <% end %> + +
+

Let's start your Forem journey!

+

Create your admin account first.

+

Then we'll walk you through your Forem setup.

+
+ +
+ <%= f.label :name, "Name", for: "name", class: "crayons-field__label" %> + <%= f.text_field :name, id: "name", placeholder: "John Doe", class: "crayons-textfield js-creator-signup-name", required: true %> + + +
+ +
js-creator-signup-username-row"> + <%= f.label :username, "Username", for: "username", class: "crayons-field__label" %> + <%= f.text_field :username, pattern: "[A-Za-z0-9_]+", maxlength: "30", title: "A username can only contain letters, numbers and underscores. The maximum username length can be 30 characters.", id: "username", class: "js-creator-signup-username crayons-textfield", + aria: { describedby: "username-helper-text" } %> +

It can only contain letters, numbers and underscores. The maximum username length can be 30 characters.

+
+ +
+ <%= f.label :email, "Email", for: "email", class: "crayons-field__label" %> + <%= f.email_field :email, placeholder: "john@example.com", autocomplete: "email", id: "email", class: "crayons-textfield", required: true %> +
+ +
+ <%= f.label :password, "Password", class: "crayons-field__label" %> +
+ <%= f.password_field :password, minlength: "8", class: "crayons-textfield js-password", placeholder: "••••••••", required: true, aria: { describedby: "password-helper-text" } %> + +
+

Minimum 8 characters

+
+ + <% needs_owner_secret_field = ENV["FOREM_OWNER_SECRET"].present? && Settings::General.waiting_on_first_user %> + <% if needs_owner_secret_field %> + <% if params[:owner_secret].present? %> + <%= f.hidden_field :owner_secret, value: params[:owner_secret] %> + <% else %> +
+ <%= f.label :owner_secret, "New Forem Secret", class: "crayons-field__label" %> + <%= f.password_field :owner_secret, class: "crayons-textfield", required: needs_owner_secret_field, placeholder: "As provided by your Forem host" %> +
+ <% end %> + <% end %> + +
+ <%= f.submit "Create my account", class: "crayons-btn" %> +
+ + <%= inline_svg_tag("forem-background.svg", aria_hidden: true, class: "forem-background absolute bottom-0 right-0") %> + <% end %> + +
+
+
diff --git a/cypress/integration/creatorOnboardingFlows/creatorSignup.spec.js b/cypress/integration/creatorOnboardingFlows/creatorSignup.spec.js new file mode 100644 index 000000000..fb91f349f --- /dev/null +++ b/cypress/integration/creatorOnboardingFlows/creatorSignup.spec.js @@ -0,0 +1,97 @@ +describe('Creator Signup Page', () => { + beforeEach(() => { + cy.testSetup(); + cy.visit('/enter?state=new-user'); + }); + + it('should display a welcome message', () => { + cy.findByText("Let's start your Forem journey!").should('be.visible'); + }); + + it('should display instructions', () => { + cy.findByText('Create your admin account first.').should('be.visible'); + cy.findByText("Then we'll walk you through your Forem setup.").should( + 'be.visible', + ); + }); + + it('should display a validated username hint that correlates to the name entered', () => { + cy.findByTestId('creator-signup-form').as('creatorSignupForm'); + cy.get('@creatorSignupForm') + .findByText(/^Name$/) + .type('1 Forem creator name! Also test a maximum length string'); + + // restricts the string to 20 characters, lowercased, + // replaces spaces and special_characters with an underscore + cy.contains('1_forem_creator_name__also_tes'); + }); + + it('should show and focus on the username field when clicking on the edit icon', () => { + cy.findByTestId('creator-signup-form').as('creatorSignupForm'); + cy.get('@creatorSignupForm') + .findByText(/^Name$/) + .type('Forem creator name'); + + cy.findByRole('button', { name: 'Edit username' }).click(); + cy.get('@creatorSignupForm') + .findByText(/^Username/) + .should('exist'); + + cy.findByLabelText('Username').should('have.value', 'forem_creator_name'); + }); + + it('should contain an email label and field', () => { + cy.findByTestId('creator-signup-form').as('creatorSignupForm'); + cy.get('@creatorSignupForm') + .findByText(/^Email/) + .should('be.visible'); + }); + + it('should contain an password', () => { + cy.findByTestId('creator-signup-form').as('creatorSignupForm'); + cy.get('@creatorSignupForm') + .findByText(/^Password/) + .should('be.visible'); + }); + + it('should toggle the password when the eye icon is clicked', () => { + cy.findByTestId('creator-signup-form').as('creatorSignupForm'); + cy.get('@creatorSignupForm') + .findByText(/^Password$/) + .type('abc123456'); + + cy.findByLabelText('Password').should('have.attr', 'type', 'password'); + cy.findByTestId('mask-icon').should('be.visible'); + cy.findByTestId('unmask-icon').should('not.be.visible'); + + cy.findByRole('button', { name: 'Show password' }).click(); + cy.findByRole('button', { name: 'Show password' }).should( + 'have.attr', + 'aria-pressed', + 'true', + ); + cy.findByLabelText('Password').should('have.attr', 'type', 'text'); + cy.findByTestId('unmask-icon').should('be.visible'); + cy.findByTestId('mask-icon').should('not.be.visible'); + }); + + it.skip("should allow sign the user in when 'Create my account' is clicked", () => { + cy.findByTestId('creator-signup-form').as('creatorSignupForm'); + cy.get('@creatorSignupForm').findByText(/^Name/).type('Forem Creator'); + + cy.get('@creatorSignupForm') + .findByText(/^Email/) + .type('forem_creator@gmail.com'); + + cy.get('@creatorSignupForm') + .findByText(/^Password/) + .type('abc123456'); + + cy.get('@creatorSignupForm') + .findByRole('button', { name: 'Create my account' }) + .click(); + + const { baseUrl } = Cypress.config(); + cy.url().should('equal', `${baseUrl}onboarding?referrer=${baseUrl}`); + }); +}); diff --git a/spec/requests/registrations_spec.rb b/spec/requests/registrations_spec.rb index bf420935a..8b2def88d 100644 --- a/spec/requests/registrations_spec.rb +++ b/spec/requests/registrations_spec.rb @@ -140,8 +140,8 @@ RSpec.describe "Registrations", type: :request do it "renders the creator onboarding form" do get root_path - expect(response.body).to include("Let's create an admin account for your community.") - expect(response.body).to include("Create admin account") + expect(response.body).to include("Let's start your Forem journey!") + expect(response.body).to include("Create your admin account first") end end end