Commit 8229b1d1 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'vslobodin/trial-workflow' into 'master'

Frontend implementation for improved trial sign-up experience for GitLab.com (SaaS) users

See merge request gitlab-org/gitlab!16732
parents 7b2bead4 64e457e8
......@@ -8,23 +8,25 @@ const USERNAME_SUGGEST_DEBOUNCE_TIME = 300;
export default class UsernameSuggester {
/**
* Creates an instance of UsernameSuggester.
* @param {HTMLElement} targetElement target input element id for suggested username
* @param {HTMLElement[]} sourceElementsIds array of HTML input element ids used for generating username
* @param {string} targetElement target input element id for suggested username
* @param {string[]} sourceElementsIds array of HTML input element ids used for generating username
*/
constructor(targetElement, sourceElementsIds = []) {
if (!targetElement) {
throw new Error(__("Required argument 'targetElement' is missing"));
throw new Error("Required argument 'targetElement' is missing");
}
this.usernameElement = document.getElementById(targetElement);
if (!this.usernameElement) {
throw new Error(__('The target element is missing.'));
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
throw new Error('The target element is missing.');
}
this.apiPath = this.usernameElement.dataset.apiPath;
if (!this.apiPath) {
throw new Error(__('The API path was not specified.'));
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
throw new Error('The API path was not specified.');
}
this.sourceElements = sourceElementsIds.map(id => document.getElementById(id)).filter(Boolean);
......
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
document.addEventListener('DOMContentLoaded', () => {
const selectElement = document.getElementById('country_select');
const { countriesEndPoint } = selectElement.dataset;
axios
.get(countriesEndPoint)
.then(({ data }) => {
// fill #country_select element with array of <option>s
data.forEach(([name, code]) => {
const option = document.createElement('option');
option.value = code;
option.text = name;
selectElement.appendChild(option);
});
})
.catch(() => new Flash(__('Error loading countries data.')));
});
import 'ee/pages/trials/country_select';
import $ from 'jquery';
document.addEventListener('DOMContentLoaded', () => {
const namespaceId = $('#namespace_id');
const newGroupName = $('#group_name');
namespaceId.on('change', () => {
const enableNewGroupName = namespaceId.val() === '0';
newGroupName
.toggleClass('hidden', !enableNewGroupName)
.find('input')
.prop('required', enableNewGroupName);
});
namespaceId.trigger('change');
});
......@@ -5,6 +5,8 @@ class TrialRegistrationsController < RegistrationsController
layout 'trial'
skip_before_action :require_no_authentication
before_action :check_if_gl_com
before_action :check_if_improved_trials_enabled
before_action :set_redirect_url, only: [:new]
......@@ -22,7 +24,11 @@ class TrialRegistrationsController < RegistrationsController
private
def set_redirect_url
store_location_for(:user, new_trial_url)
if user_signed_in?
redirect_to new_trial_url
else
store_location_for(:user, new_trial_url)
end
end
def skip_confirmation
......
# frozen_string_literal: true
class TrialsController < ApplicationController
include ActionView::Helpers::SanitizeHelper
layout 'trial'
before_action :check_if_gl_com
before_action :check_if_improved_trials_enabled
before_action :authenticate_user!
before_action :fetch_namespace, only: :apply
before_action :find_or_create_namespace, only: :apply
def new
end
......@@ -13,9 +17,9 @@ class TrialsController < ApplicationController
end
def create_lead
result = GitlabSubscriptions::CreateLeadService.new.execute({ trial_user: company_params })
@lead_result = GitlabSubscriptions::CreateLeadService.new.execute({ trial_user: company_params })
if result[:success]
if @lead_result[:success]
redirect_to select_trials_url
else
render :new
......@@ -23,12 +27,14 @@ class TrialsController < ApplicationController
end
def apply
result = GitlabSubscriptions::ApplyTrialService.new.execute(apply_trial_params)
return render(:select) if @namespace.invalid?
@trial_result = GitlabSubscriptions::ApplyTrialService.new.execute(apply_trial_params)
if result[:success]
if @trial_result&.dig(:success)
redirect_to group_url(@namespace, { trial: true })
else
redirect_to select_trials_url
render :select
end
end
......@@ -69,9 +75,30 @@ class TrialsController < ApplicationController
}
end
def fetch_namespace
@namespace = current_user.namespaces.find(params[:namespace_id])
def find_or_create_namespace
@namespace = if find_namespace?
current_user.namespaces.find_by_id(params[:namespace_id])
elsif can_create_group?
create_group
end
render_404 unless @namespace
end
def find_namespace?
params[:namespace_id].present? && params[:namespace_id] != '0'
end
def can_create_group?
params[:new_group_name].present? && can?(current_user, :create_group)
end
def create_group
name = sanitize(params[:new_group_name])
group = Groups::CreateService.new(current_user, name: name, path: name.parameterize).execute
params[:namespace_id] = group.id if group.persisted?
group
end
end
# frozen_string_literal: true
module EE
module TrialHelper
def company_size_options_for_select(selected = 0)
options_for_select([
[_('Please select'), 0],
['1 - 99', '1-99'],
['100 - 499', '100-499'],
['500 - 1,999', '500-1,999'],
['2,000 - 9,999', '2,000-9,999'],
['10,000 +', '10,000+']
], selected)
end
def namespace_options_for_select
groups = current_user.manageable_groups.map { |g| [g.name, g.id] }
users = [[current_user.namespace.name, current_user.namespace_id]]
grouped_options = {
'New' => [[_('Create group'), 0]],
'Groups' => groups,
'Users' => users
}
grouped_options_for_select(grouped_options, nil, prompt: _('Please select'))
end
def show_trial_errors?(namespace, trial_result)
namespace&.invalid? || !trial_result&.dig(:success)
end
def trial_errors(namespace, trial_result)
namespace&.errors&.full_messages&.to_sentence&.presence || trial_result&.dig(:errors)&.presence
end
end
end
......@@ -11,11 +11,11 @@ module GitlabSubscriptions
{ success: false, errors: response.dig(:data, :errors) }
end
end
end
private
private
def client
Gitlab::SubscriptionPortal::Client.new
def client
Gitlab::SubscriptionPortal::Client.new
end
end
end
......@@ -2,9 +2,9 @@
- max_username_length = 255
#register-pane.tab-pane.login-box{ role: 'tabpanel' }
.login-body
= form_for(User.new, as: :new_user, url: trial_registrations_path, html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive' }) do |f|
= form_for(user, as: :new_user, url: trial_registrations_path, html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive' }) do |f|
.devise-errors
= render 'devise/shared/error_messages', resource: User.new
= render 'devise/shared/error_messages', resource: user
- if Feature.enabled?(:invisible_captcha)
= invisible_captcha
.name.form-row
......
......@@ -14,7 +14,7 @@
-# Signup only makes sense if you can also sign-in
- if allow_signup?
= render 'signup_box'
= render 'signup_box', user: resource
-# Show a message if none of the mechanisms above are enabled
- if !password_authentication_enabled_for_web? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
......
.container.fixed-bottom.mb-5
.mw-460.mx-auto
= link_to _('Skip Trial (Continue with Free Account)'), dashboard_projects_path, class: 'block center py-2'
.label
= _("You won't get a free trial right now but you can always resume this process by clicking on your avatar and choosing 'Start a free trial'.")
- page_title _('Start a Free Trial')
%h3.center.pt-6
= _('Start a Free Trial')
%p.center
= _('We need some additional information to activate your free trial')
- if @lead_result&.dig(:errors).present?
.flash-container
.flash-alert.text-center
= _('We have found the following errors:')
.flash-text
= @lead_result[:errors]
= form_tag create_lead_trials_path, method: :post do |f|
.form-group
= label_tag :company_name, _('Company name'), for: :company_name, class: 'col-form-label'
= text_field_tag :company_name, params[:company_name], class: 'form-control', required: true
.form-group
= label_tag :company_size, _('Number of employees?'), for: :company_size, class: 'col-form-label'
= select_tag :company_size, company_size_options_for_select(params[:company_size]), include_blank: true, class: 'select2', required: true
.form-group
= label_tag :phone_number, _('Telephone number'), for: :phone_number, class: 'col-form-label'
= text_field_tag :phone_number, params[:phone_number], class: 'form-control', required: true
.form-group
= label_tag :number_of_users, _('How many users will be evaluating the trial?'), for: :number_of_users, class: 'col-form-label'
= number_field_tag :number_of_users, nil, class: 'form-control', required: true
.form-group
= label_tag :country, _('Country'), class: 'col-form-label'
= select_tag :country, options_for_select([[_('Please select a country'), '']]), class: 'select2', required: true, id: 'country_select', data: { countries_end_point: countries_path }
= submit_tag _('Continue'), class: 'btn btn-success btn-block'
= render 'skip_trial'
- page_title _('Start a Free Trial')
%h3.center.pt-6
= _('Almost there')
%p.center
= _('You can apply your Trial to your Personal account or create a New Group.')
- if show_trial_errors?(@namespace, @trial_result)
.flash-container
.flash-alert.text-center
= _('We have found the following errors:')
.flash-text
= trial_errors(@namespace, @trial_result)
= form_tag apply_trials_path, method: :post do
.form-group
= label_tag :namespace_id, _('This subscription is for'), for: :namespace_id, class: 'col-form-label'
= select_tag :namespace_id, namespace_options_for_select, class: 'select2', required: true
#group_name.form-group.hidden
= label_tag :new_group_name, _('New Group Name'), for: :new_group_name, class: 'col-form-label'
= text_field_tag :new_group_name, nil, class: 'form-control'
= submit_tag _('Start your free trial'), class: 'btn btn-success btn-block'
= render 'skip_trial'
---
title: Frontend implementation for improved trial sign-up experience for GitLab.com (SaaS) users
merge_request: 16732
author:
type: added
......@@ -4,6 +4,8 @@ require 'spec_helper'
describe TrialRegistrationsController do
describe '#new' do
let(:user) { create(:user) }
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
......@@ -19,6 +21,26 @@ describe TrialRegistrationsController do
expect(response).to redirect_to("#{EE::SUBSCRIPTIONS_URL}/trials/new?gl_com=true")
end
end
context 'when customer is authenticated' do
before do
sign_in(user)
end
it 'redirects to the new trial page' do
get :new
expect(response).to redirect_to(new_trial_url)
end
end
context 'when customer is not authenticated' do
it 'renders the regular template' do
get :new
expect(response).to render_template(:new)
end
end
end
describe '#create' do
......
......@@ -89,7 +89,7 @@ describe TrialsController do
before do
sign_in(user)
expect_any_instance_of(GitlabSubscriptions::ApplyTrialService).to receive(:execute) do
allow_any_instance_of(GitlabSubscriptions::ApplyTrialService).to receive(:execute) do
{ success: apply_trial_result }
end
end
......@@ -102,15 +102,32 @@ describe TrialsController do
expect(response).to redirect_to("/#{namespace.path}?trial=true")
end
context 'with a new Group' do
it 'creates the Group' do
expect do
post :apply, params: { new_group_name: 'GitLab' }
end.to change { Group.count }.to(1)
end
end
end
context 'on failure' do
let(:apply_trial_result) { false }
it 'redirects to new select namespaces for trials path' do
it 'renders the :select view' do
post :apply, params: { namespace_id: namespace.id }
expect(response).to redirect_to(select_trials_path)
expect(response).to render_template(:select)
end
context 'with a new Group' do
it 'renders the :select view' do
post :apply, params: { new_group_name: 'admin' }
expect(response).to render_template(:select)
expect(Group.count).to eq(0)
end
end
end
end
......
......@@ -16,17 +16,66 @@ describe 'Trial Sign Up', :js do
let(:existing_user) { create(:user) }
it 'shows the error about existing username' do
visit(new_trial_registration_path)
visit new_trial_registration_path
click_on 'Register'
within('div#register-pane') do
fill_in 'new_user_username', with: existing_user.username
fill_in 'new_user_username', with: existing_user[:username]
end
expect(page).to have_content('Username is already taken.')
end
end
context 'with the available username' do
it 'registers the user and proceeds to the next step' do
visit new_trial_registration_path
click_on 'Register'
within('div#register-pane') do
fill_in 'new_user_first_name', with: user_attrs[:first_name]
fill_in 'new_user_last_name', with: user_attrs[:last_name]
fill_in 'new_user_username', with: user_attrs[:username]
fill_in 'new_user_email', with: user_attrs[:email]
fill_in 'new_user_password', with: user_attrs[:password]
check 'terms_opt_in'
click_button 'Continue'
end
wait_for_requests
expect(current_path).to eq(new_trial_path)
expect(page).to have_content('Start a Free Trial')
end
end
context 'entering' do
using RSpec::Parameterized::TableSyntax
where(:case_name, :first_name, :last_name, :suggested_username) do
'first name' | 'foobar' | nil | 'foobar'
'last name' | nil | 'foobar' | 'foobar'
'first name and last name' | 'foo' | 'bar' | 'foo_bar'
end
with_them do
it 'suggests the username' do
visit new_trial_registration_path
click_on 'Register'
within('div#register-pane') do
fill_in 'new_user_first_name', with: first_name if first_name
fill_in 'new_user_last_name', with: last_name if last_name
end
find('body').click
expect(page).to have_field('new_user_username', with: suggested_username)
end
end
end
context 'entering' do
using RSpec::Parameterized::TableSyntax
......@@ -38,7 +87,7 @@ describe 'Trial Sign Up', :js do
with_them do
it 'suggests the username' do
visit(new_trial_registration_path)
visit new_trial_registration_path
click_on 'Register'
within('div#register-pane') do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Trial Capture Lead', :js do
include Select2Helper
let(:user) { create(:user) }
before do
stub_feature_flags(invisible_captcha: false)
stub_feature_flags(improved_trial_signup: true)
allow(Gitlab).to receive(:com?).and_return(true).at_least(:once)
sign_in(user)
end
context 'when user' do
before do
visit new_trial_path
wait_for_requests
end
context 'enters valid company information' do
before do
expect_any_instance_of(GitlabSubscriptions::CreateLeadService).to receive(:execute) do
{ success: true }
end
end
it 'proceeds to the next step' do
fill_in 'company_name', with: 'GitLab'
select2 '1-99', from: '#company_size'
fill_in 'phone_number', with: '+1234567890'
fill_in 'number_of_users', with: '1'
select2 'US', from: '#country_select'
click_button 'Continue'
expect(page).not_to have_css('flash-container')
expect(current_path).to eq(select_trials_path)
end
end
context 'enters invalid company information' do
before do
fill_in 'company_name', with: 'GitLab'
select2 '1-99', from: '#company_size'
# to trigger validation error
# skip filling phone number
# fill_in 'phone_number', with: '+1234567890'
fill_in 'number_of_users', with: '1'
select2 'US', from: '#country_select'
click_button 'Continue'
end
it 'shows validation error' do
message = page.find('#phone_number').native.attribute('validationMessage')
expect(message).to eq('Please fill out this field.')
end
it 'does not proceeds to the next step' do
expect(current_path).to eq(new_trial_path)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Trial Select Namespace', :js do
include Select2Helper
let(:new_group_name) { 'GitLab' }
let(:user) { create(:user) }
before do
stub_feature_flags(invisible_captcha: false)
stub_feature_flags(improved_trial_signup: true)
allow(Gitlab).to receive(:com?).and_return(true).at_least(:once)
sign_in(user)
end
context 'when user' do
context 'selects create a new group' do
before do
visit select_trials_path
wait_for_all_requests
select2 '0', from: '#namespace_id'
end
it 'shows the new group name input' do
expect(page).to have_field('New Group Name')
end
context 'enters a valid new group name' do
context 'when user can create groups' do
before do
expect_any_instance_of(GitlabSubscriptions::ApplyTrialService).to receive(:execute) do
{ success: true }
end
end
it 'proceeds to the next step' do
fill_in 'New Group Name', with: new_group_name
click_button 'Start your free trial'
wait_for_requests
expect(page).not_to have_css('flash-container')
expect(current_path).to eq('/gitlab')
end
end
context 'when user can not create groups' do
before do
user.update_attribute(:can_create_group, false)
end
it 'returns 404' do
fill_in 'New Group Name', with: new_group_name
click_button 'Start your free trial'
expect(page).to have_content('Page Not Found')
end
end
end
context 'enters an existing group name' do
let!(:namespace) { create(:namespace, owner_id: user.id, path: 'gitlab') }
it 'shows validation error' do
fill_in 'New Group Name', with: namespace.path
click_button 'Start your free trial'
wait_for_requests
expect(page).to have_selector('.flash-text')
expect(find('.flash-alert')).to have_text('Group URL has already been taken')
expect(current_path).to eq(apply_trials_path)
end
end
context 'and does not enter a new group name' do
it 'shows validation error' do
click_button 'Start your free trial'
message = page.find('#new_group_name').native.attribute('validationMessage')
expect(message).to eq('Please fill out this field.')
expect(current_path).to eq(select_trials_path)
end
end
end
context 'selects an existing user' do
before do
visit select_trials_path
wait_for_all_requests
select2 user.namespace.id, from: '#namespace_id'
end
it 'does not show the new group name input' do
expect(page).not_to have_field('New Group Name')
end
it 'applies trial and redirects to dashboard' do
expect_any_instance_of(GitlabSubscriptions::ApplyTrialService).to receive(:execute) do
{ success: true }
end
click_button 'Start your free trial'
wait_for_requests
expect(current_path).to eq("/#{user.namespace.path}")
end
end
end
end
......@@ -1344,6 +1344,9 @@ msgstr ""
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
msgid "Almost there"
msgstr ""
msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
msgstr ""
......@@ -4043,6 +4046,9 @@ msgstr ""
msgid "Company"
msgstr ""
msgid "Company name"
msgstr ""
msgid "Compare"
msgstr ""
......@@ -4426,6 +4432,9 @@ msgstr ""
msgid "Could not save prometheus manual configuration"
msgstr ""
msgid "Country"
msgstr ""
msgid "Coverage"
msgstr ""
......@@ -6171,6 +6180,9 @@ msgstr ""
msgid "Error loading burndown chart data"
msgstr ""
msgid "Error loading countries data."
msgstr ""
msgid "Error loading file viewer."
msgstr ""
......@@ -8293,6 +8305,9 @@ msgstr ""
msgid "How many shards to split the Elasticsearch index over."
msgstr ""
msgid "How many users will be evaluating the trial?"
msgstr ""
msgid "However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation."
msgstr ""
......@@ -10378,6 +10393,9 @@ msgstr ""
msgid "New Group"
msgstr ""
msgid "New Group Name"
msgstr ""
msgid "New Identity"
msgstr ""
......@@ -10866,6 +10884,9 @@ msgstr ""
msgid "Number of commits per MR"
msgstr ""
msgid "Number of employees?"
msgstr ""
msgid "Number of files touched"
msgstr ""
......@@ -11627,6 +11648,12 @@ msgstr ""
msgid "Please retype the email address."
msgstr ""
msgid "Please select"
msgstr ""
msgid "Please select a country"
msgstr ""
msgid "Please select a file"
msgstr ""
......@@ -13435,9 +13462,6 @@ msgstr ""
msgid "Require users to prove ownership of custom domains"
msgstr ""
msgid "Required argument 'targetElement' is missing"
msgstr ""
msgid "Requires approval from %{names}."
msgid_plural "Requires %{count} more approvals from %{names}."
msgstr[0] ""
......@@ -14686,6 +14710,9 @@ msgstr ""
msgid "Size limit per repository (MB)"
msgstr ""
msgid "Skip Trial (Continue with Free Account)"
msgstr ""
msgid "Skip this for now"
msgstr ""
......@@ -15160,6 +15187,9 @@ msgstr ""
msgid "Start thread & reopen %{noteable_name}"
msgstr ""
msgid "Start your free trial"
msgstr ""
msgid "Start your trial"
msgstr ""
......@@ -15622,6 +15652,9 @@ msgstr ""
msgid "Team domain"
msgstr ""
msgid "Telephone number"
msgstr ""
msgid "Template"
msgstr ""
......@@ -15696,9 +15729,6 @@ msgid_plural "The %{type} contains the following errors:"
msgstr[0] ""
msgstr[1] ""
msgid "The API path was not specified."
msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
......@@ -15933,9 +15963,6 @@ msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr ""
msgid "The target element is missing."
msgstr ""
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr ""
......@@ -16365,6 +16392,9 @@ msgstr ""
msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}"
msgstr ""
msgid "This subscription is for"
msgstr ""
msgid "This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like \"1 hour\". Values without specification represent seconds."
msgstr ""
......@@ -17959,9 +17989,15 @@ msgstr ""
msgid "We don't have enough data to show this stage."
msgstr ""
msgid "We have found the following errors:"
msgstr ""
msgid "We heard back from your U2F device. You have been authenticated."
msgstr ""
msgid "We need some additional information to activate your free trial"
msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
......@@ -18288,6 +18324,9 @@ msgstr ""
msgid "You can also upload existing files from your computer using the instructions below."
msgstr ""
msgid "You can apply your Trial to your Personal account or create a New Group."
msgstr ""
msgid "You can create files directly in GitLab using one of the following options."
msgstr ""
......@@ -18528,6 +18567,9 @@ msgstr ""
msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
msgstr ""
msgid "You won't get a free trial right now but you can always resume this process by clicking on your avatar and choosing 'Start a free trial'."
msgstr ""
msgid "You'll be signed out from your current account automatically."
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment