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] %>
+
+ <%= 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 %>
+
+
+
+
+ <% resource.errors.full_messages.each do |message| %>
+ <%= message %>
+ <% end %>
+
+
+
+ <% 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 %>
+
+
+
+ Your username will be
+
+
+ <%= inline_svg_tag("pencil.svg", class: "cursor-pointer crayons-icon opacity-75 ml-1", aria_hidden: true, title: "Edit username") %>
+
+
+
+
+
+
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" } %>
+
+ <%= inline_svg_tag("eye.svg", class: "crayons-icon js-eye", data: { testid: "mask-icon" }, aria_hidden: true, title: "Show password") %>
+ <%= inline_svg_tag("eye-off.svg", class: "crayons-icon hidden js-eye-off", data: { testid: "unmask-icon" }, aria_hidden: true, title: "Hide password") %>
+
+
+
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