Commit 8d99ce59 authored by Alper Akgun's avatar Alper Akgun Committed by Martin Wortschack

Add optional "start a trial" in experimental free SaaS signup

Growth conversion experiment behind a feature flag
parent 6cf182a3
......@@ -3,13 +3,13 @@
* MR link https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22716
*/
.gl-select2-html5-required-fix div.select2-container+select.select2 {
@include gl-opacity-0;
@include gl-border-0;
@include gl-bg-none;
@include gl-bg-transparent;
display: block !important;
width: 1px;
height: 1px;
z-index: -1;
opacity: 0;
margin: -3px auto 0;
background-image: none;
background-color: transparent;
border: 0;
}
......@@ -4,21 +4,24 @@ import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
const selectElement = document.getElementById('country_select');
const { countriesEndPoint, selectedOption } = 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;
if (selectElement?.dataset) {
const { countriesEndPoint, selectedOption } = selectElement.dataset;
selectElement.appendChild(option);
});
$(selectElement)
.val(selectedOption)
.trigger('change.select2');
})
.catch(() => new Flash(__('Error loading countries data.')));
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);
});
$(selectElement)
.val(selectedOption)
.trigger('change.select2');
})
.catch(() => new Flash(__('Error loading countries data.')));
}
<script>
import { GlToggle } from '@gitlab/ui';
export default {
components: {
GlToggle,
},
props: {
active: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
trial: this.active,
};
},
mounted() {
this.toggleTrial();
},
methods: {
toggleTrial() {
this.$emit('changed', {
trial: this.trial,
});
},
},
};
</script>
<template>
<gl-toggle v-model="trial" name="trial" data-testid="trial" @change="toggleTrial" />
</template>
import Vue from 'vue';
import mountInviteMembers from 'ee/groups/invite';
import mountVisibilityLevelDropdown from '~/groups/visibility_level';
import 'ee/pages/trials/country_select';
import { STEPS, ONBOARDING_ISSUES_EXPERIMENT_FLOW_STEPS } from '../../constants';
import ProgressBar from '../../components/progress_bar.vue';
import RegistrationTrialToggle from '../../components/registration_trial_toggle.vue';
function mountProgressBar() {
const el = document.getElementById('progress-bar');
if (!el) return null;
if (!el) {
return null;
}
return new Vue({
el,
......@@ -19,8 +23,47 @@ function mountProgressBar() {
});
}
function toggleTrialForm(trial) {
const form = document.querySelector('.js-trial-form');
const fields = document.querySelectorAll('.js-trial-field');
if (!form) {
return null;
}
form.classList.toggle('hidden', !trial);
fields.forEach(f => {
f.disabled = !trial; // eslint-disable-line no-param-reassign
});
return trial;
}
function mountTrialToggle() {
const el = document.querySelector('.js-trial-toggle');
if (!el) {
return null;
}
const { active } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(RegistrationTrialToggle, {
props: { active },
on: {
changed: event => toggleTrialForm(event.trial),
},
});
},
});
}
export default () => {
mountProgressBar();
mountVisibilityLevelDropdown();
mountInviteMembers();
mountTrialToggle();
};
......@@ -12,16 +12,26 @@ module Registrations
feature_category :navigation
def new
record_experiment_user(:trial_during_signup)
@group = Group.new(visibility_level: helpers.default_group_visibility)
end
def create
@group = Groups::CreateService.new(current_user, group_params).execute
trial = params[:trial] == 'true'
if @group.persisted?
invite_members(@group)
record_experiment_user(:trial_during_signup, trial_chosen: trial)
redirect_to new_users_sign_up_project_path(namespace_id: @group.id)
if experiment_enabled?(:trial_during_signup)
if trial && create_lead && apply_trial
record_experiment_conversion_event(:trial_during_signup)
end
else
invite_members(@group)
end
redirect_to new_users_sign_up_project_path(namespace_id: @group.id, trial: trial)
else
render action: :new
end
......@@ -40,5 +50,42 @@ module Registrations
def group_params
params.require(:group).permit(:name, :path, :visibility_level)
end
def create_lead
trial_params = {
trial_user: params.permit(
:company_name,
:company_size,
:phone_number,
:number_of_users,
:country
).merge(
work_email: current_user.email,
first_name: current_user.first_name,
last_name: current_user.last_name,
uid: current_user.id,
skip_email_confirmation: true,
gitlab_com_trial: true,
provider: 'gitlab',
newsletter_segment: current_user.email_opted_in
)
}
result = GitlabSubscriptions::CreateLeadService.new.execute(trial_params)
result[:success]
end
def apply_trial
apply_trial_params = {
uid: current_user.id,
trial_user: {
namespace_id: @group.id,
gitlab_com_trial: true,
sync_to_gl: true
}
}
result = GitlabSubscriptions::ApplyTrialService.new.execute(apply_trial_params)
result&.dig(:success)
end
end
end
......@@ -4,5 +4,5 @@
%body.ui-indigo.d-flex.vh-100
= render "layouts/header/logo_with_title"
= render "layouts/broadcast"
.container.d-flex.flex-grow-1.m-0
.container.d-flex.gl-flex-direction-column.m-0
= yield
......@@ -40,7 +40,10 @@
.row
.form-group.col-sm-12
= render partial: 'shared/groups/visibility_level', locals: { f: f }
= render partial: 'shared/groups/invite_members'
- if experiment_enabled?(:trial_during_signup)
= render partial: 'shared/groups/trial_form'
- else
= render partial: 'shared/groups/invite_members'
.row
.form-group.col-sm-12.mb-0
= button_tag class: %w[btn btn-success w-100] do
......
%section.gl-banner.gl-banner-introduction{ data: { uid: 'trial_success_banner_dismissed' } }
.gl-banner-illustration
= image_tag('illustrations/illustration-congratulation-purchase.svg', alt: s_('Trial|Successful trial activation image'))
.gl-banner-content.gl-my-auto
%h3.gl-banner-title= _('Congratulations, your free trial is activated.')
%button.gl-banner-close.close.js-close-session.js-close-callout{ type: 'button', 'aria-label' => s_('Trial|Dismiss') }
= sprite_icon('close', css_class: 'dismiss-icon')
- page_title _('Your first project')
- visibility_level = selected_visibility_level(@project, params.dig(:project, :visibility_level))
- if params[:trial] == 'true'
.row.bg-gray-light
.d-flex.flex-column.align-items-center.w-100
= render 'trial_is_activated_banner'
.row.flex-grow-1.bg-gray-light
.d-flex.flex-column.align-items-center.w-100.p-3
.new-project.d-flex.flex-column.align-items-center.pt-5
......
.row
.form-group.col-sm-12
= label_tag :trial_toggle, _('GitLab Gold trial (optional)'), for: :trial_toggle, class: 'col-form-label'
%p= html_escape(('Try all GitLab features for free for 30 days.%{br_tag}No credit card required.')) % { br_tag: '<br/>'.html_safe }
.js-trial-toggle{ data: { active: params[:trial] == 'true' } }
%p.gl-text-gray-500.gl-mt-3= _('We will activate your trial on your group once you complete this step. After 30 days, you can upgrade to any plan')
.js-trial-form.hidden
.row
.form-group.col-sm-12
= label_tag :company_name, _('Company name'), for: :company_name, class: 'col-form-label'
= text_field_tag :company_name, params[:company_name], class: 'form-control js-trial-field', required: true
.row
.form-group.col-sm-12.gl-select2-html5-required-fix
= 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: 'js-trial-field select2', required: true
.row
.form-group.col-sm-12
= label_tag :number_of_users, _('How many employees will use Gitlab?'), for: :number_of_users, class: 'col-form-label'
= number_field_tag :number_of_users, params[:number_of_users], class: 'form-control js-trial-field', required: true, min: 1
.row
.form-group.col-sm-12
= label_tag :phone_number, _('Telephone number'), for: :phone_number, class: 'col-form-label'
= text_field_tag :phone_number, params[:phone_number], class: 'form-control js-trial-field', required: true
.row
.form-group.col-sm-12.gl-select2-html5-required-fix
= label_tag :country, _('Country'), class: 'col-form-label'
= select_tag :country, options_for_select([[_('Please select a country'), '']]), class: ' js-trial-field select2 gl-transparent-pixel', required: true, id: 'country_select', data: { countries_end_point: countries_path, selected_option: params[:country]}
......@@ -29,6 +29,12 @@ RSpec.describe Registrations::GroupsController do
expect(assigns(:group).visibility_level).to eq(Gitlab::CurrentSettings.default_group_visibility)
end
it 'calls the record user method for trial_during_signup experiment' do
expect(controller).to receive(:record_experiment_user).with(:trial_during_signup)
subject
end
context 'user without the ability to create a group' do
let(:user) { create(:user, can_create_group: false) }
......@@ -50,7 +56,9 @@ RSpec.describe Registrations::GroupsController do
{ name: 'Group name', path: 'group-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE, emails: ['', ''] }
end
subject { post :create, params: { group: group_params } }
subject { post :create, params: { group: group_params }.merge(trial_form_params) }
let_it_be(:trial_form_params) { { trial: 'false' } }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
......@@ -68,7 +76,93 @@ RSpec.describe Registrations::GroupsController do
end
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: user.groups.last.id)) }
it { is_expected.to redirect_to(new_users_sign_up_project_path(namespace_id: user.groups.last.id, trial: false)) }
it 'calls the record user trial_during_signup experiment' do
expect(controller).to receive(:record_experiment_user).with(:trial_during_signup, trial_chosen: false)
subject
end
context 'in experiment group for trial_during_signup' do
let_it_be(:group) { create(:group) }
let_it_be(:trial_form_params) do
{
trial: 'true',
company_name: 'ACME',
company_size: '1-99',
phone_number: '11111111',
number_of_users: '17',
country: 'Norway'
}
end
let_it_be(:trial_user_params) do
{
work_email: user.email,
first_name: user.first_name,
last_name: user.last_name,
uid: user.id,
skip_email_confirmation: true,
gitlab_com_trial: true,
provider: 'gitlab',
newsletter_segment: user.email_opted_in
}
end
let_it_be(:trial_params) do
{
trial_user: ActionController::Parameters.new(trial_form_params.except(:trial).merge(trial_user_params)).permit!
}
end
let_it_be(:apply_trial_params) do
{
uid: user.id,
trial_user: {
namespace_id: group.id,
gitlab_com_trial: true,
sync_to_gl: true
}
}
end
before do
allow(controller).to receive(:experiment_enabled?).with(:onboarding_issues).and_call_original
allow(controller).to receive(:experiment_enabled?).with(:trial_during_signup).and_return(true)
end
it 'calls the lead creation and trial apply services' do
expect_next_instance_of(Groups::CreateService) do |service|
expect(service).to receive(:execute).and_return(group)
end
expect_next_instance_of(GitlabSubscriptions::CreateLeadService) do |service|
expect(service).to receive(:execute).with(trial_params).and_return(success: true)
end
expect_next_instance_of(GitlabSubscriptions::ApplyTrialService) do |service|
expect(service).to receive(:execute).with(apply_trial_params).and_return({ success: true })
end
subject
end
context 'when user chooses no trial' do
let_it_be(:trial_form_params) { { trial: 'false' } }
it 'calls the record user trial_during_signup experiment' do
expect(controller).to receive(:record_experiment_user).with(:trial_during_signup, trial_chosen: false)
subject
end
it 'does not call trial_during_signup experiment methods' do
expect(controller).not_to receive(:create_lead)
expect(controller).not_to receive(:apply_trial)
subject
end
end
end
it_behaves_like GroupInviteMembers
......@@ -80,6 +174,13 @@ RSpec.describe Registrations::GroupsController do
expect(assigns(:group).errors).not_to be_blank
end
it 'does not call trial_during_signup experiment methods' do
expect(controller).not_to receive(:create_lead)
expect(controller).not_to receive(:apply_trial)
subject
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template(:new) }
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User sees new onboarding flow', :js do
include Select2Helper
let_it_be(:user) { create(:user) }
before do
allow(Gitlab).to receive(:com?).and_return(true)
stub_experiment(onboarding_issues: true, trial_during_signup: true)
stub_experiment_for_subject(onboarding_issues: true, trial_during_signup: true)
sign_in(user)
visit users_sign_up_welcome_path
expect(page).to have_content('Welcome to GitLab')
choose 'Just me'
click_on 'Continue'
expect(page).to have_content('GitLab Gold trial (optional)')
end
it 'shows the expected behavior with no trial chosen' do
fill_in 'group_name', with: 'test'
click_on 'Create group'
expect(page).not_to have_content('Congratulations, your free trial is activated.')
expect(page).to have_content('Create/import your first project')
end
it 'shows the expected behavior with trial chosen' do
fields = ['Company name', 'Number of employees', 'How many employees will use Gitlab?', 'Telephone number', 'Country']
fill_in 'group_name', with: 'test'
# fields initially invisible
fields.each { |field| expect(page).not_to have_content(field) }
# fields become visible with trial toggle
click_button class: 'gl-toggle'
fields.each { |field| expect(page).to have_content(field) }
# fields are required
click_on 'Create group'
expect(page).to have_content('This field is required')
# make fields invisible again
click_button class: 'gl-toggle'
fields.each { |field| expect(page).not_to have_content(field) }
# make fields visible again
click_button class: 'gl-toggle'
fields.each { |field| expect(page).to have_content(field) }
# submit the trial form
fill_in 'company_name', with: 'GitLab'
select2 '1-99', from: '#company_size'
fill_in 'number_of_users', with: '1'
fill_in 'phone_number', with: '+1234567890'
select2 'US', from: '#country_select'
expect_next_instance_of(GitlabSubscriptions::CreateLeadService) do |service|
expect(service).to receive(:execute).and_return(success: true)
end
expect_next_instance_of(GitlabSubscriptions::ApplyTrialService) do |service|
expect(service).to receive(:execute).and_return({ success: true })
end
click_on 'Create group'
expect(page).to have_content('Congratulations, your free trial is activated.')
expect(page).to have_content('Create/import your first project')
end
end
import { GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RegistrationTrialToggle from 'ee/registrations/components/registration_trial_toggle.vue';
describe('Registration Trial Toggle', () => {
let wrapper;
const createComponent = propsData => {
wrapper = shallowMount(RegistrationTrialToggle, {
propsData,
});
};
beforeEach(() => {
createComponent({ active: false });
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('Default state', () => {
it('renders component properly', () => {
expect(wrapper.find(RegistrationTrialToggle).exists()).toBe(true);
});
it('shows the toggle component', () => {
expect(wrapper.find(GlToggle).exists()).toBe(true);
});
it('sets the default value to be false', () => {
expect(wrapper.vm.trial).toBe(false);
});
});
describe('Emits events', () => {
it('emits initial event', () => {
expect(wrapper.emitted().changed).toEqual([[{ trial: false }]]);
});
it('emits another event', () => {
wrapper.find(GlToggle).vm.$emit('change', true);
expect(wrapper.vm.trial).toBe(true);
expect(wrapper.emitted().changed).toEqual([[{ trial: false }], [{ trial: true }]]);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'registrations/groups/new' do
let_it_be(:user) { create(:user) }
let_it_be(:trial_during_signup) { false }
before do
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:experiment_enabled?).with(:trial_during_signup).and_return(trial_during_signup)
@group = create(:group)
render
end
subject { rendered }
context 'feature flag trial_during_signup is enabled' do
let_it_be(:trial_during_signup) { true }
it 'shows trial form and hides invite members' do
is_expected.to have_content('Company name')
is_expected.not_to have_selector('.js-invite-members')
end
end
context 'feature flag trial_during_signup is disabled' do
it 'shows trial form and hides invite members' do
is_expected.not_to have_content('Company name')
is_expected.to have_selector('.js-invite-members')
end
end
end
......@@ -87,6 +87,9 @@ module Gitlab
},
invite_members_empty_project_version_a: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyProjectVersionA'
},
trial_during_signup: {
tracking_category: 'Growth::Conversion::Experiment::TrialDuringSignup'
}
}.freeze
......
......@@ -7329,6 +7329,9 @@ msgstr ""
msgid "Congratulations! You have enabled Two-factor Authentication!"
msgstr ""
msgid "Congratulations, your free trial is activated."
msgstr ""
msgid "Connect"
msgstr ""
......@@ -12977,6 +12980,9 @@ msgstr ""
msgid "GitLab Billing Team."
msgstr ""
msgid "GitLab Gold trial (optional)"
msgstr ""
msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
......@@ -14223,6 +14229,9 @@ msgstr ""
msgid "How many days need to pass between marking entity for deletion and actual removing it."
msgstr ""
msgid "How many employees will use Gitlab?"
msgstr ""
msgid "How many replicas each Elasticsearch shard has."
msgstr ""
......@@ -29419,6 +29428,12 @@ msgstr ""
msgid "Trials|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 "Trial|Dismiss"
msgstr ""
msgid "Trial|Successful trial activation image"
msgstr ""
msgid "Trigger"
msgstr ""
......@@ -31055,6 +31070,9 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
msgid "We will activate your trial on your group once you complete this step. After 30 days, you can upgrade to any plan"
msgstr ""
msgid "We will notify %{inviter} that you declined their invitation to join GitLab. You will stop receiving reminders."
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