diff --git a/app/views/layouts/minimal.html.haml b/app/views/layouts/minimal.html.haml index fa39b6d45baa1e44324190110c197dd4c09685ad..73ae26b1940d1b4fbe790d2d9119f292cea403bc 100644 --- a/app/views/layouts/minimal.html.haml +++ b/app/views/layouts/minimal.html.haml @@ -1,10 +1,11 @@ - @with_header = true - page_classes = page_class.push(@html_class).flatten.compact +- body_classes = [system_message_class, @body_class] !!! 5 %html.gl-h-full{ lang: I18n.locale, class: page_classes } = render "layouts/head" - %body.gl-h-full{ data: body_data, class: system_message_class } + %body.gl-h-full{ data: body_data, class: body_classes } = header_message = render 'peek/bar' = render 'layouts/published_experiments' diff --git a/config/initializers_before_autoloader/000_inflections.rb b/config/initializers_before_autoloader/000_inflections.rb index 6f73dbb4d8a7075e3103652d301f0e8daccb48a8..7723cfbcf9c4ddd79b5bda751f1171137abd8b7a 100644 --- a/config/initializers_before_autoloader/000_inflections.rb +++ b/config/initializers_before_autoloader/000_inflections.rb @@ -20,6 +20,7 @@ dependency_proxy_blob_registry design_management_repository_registry dependency_proxy_manifest_registry + duo_pro event_log file_registry group_view diff --git a/ee/app/assets/images/illustrations/bg-decorations.svg b/ee/app/assets/images/illustrations/bg-decorations.svg new file mode 100644 index 0000000000000000000000000000000000000000..3aba9bdc2d0edcab3e5f9c7772c48d0dbc409fd0 --- /dev/null +++ b/ee/app/assets/images/illustrations/bg-decorations.svg @@ -0,0 +1 @@ +<svg width="1074" height="471" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M935.494.813l-1.146 6.16c-.375 2.006-1.979 3.561-3.999 3.93l-6.27 1.125c-1.104.205-1.104 1.76 0 1.944l6.27 1.126c2.041.368 3.624 1.944 3.999 3.929l1.146 6.16c.208 1.084 1.791 1.084 1.979 0l1.145-6.16c.375-2.006 1.979-3.561 4-3.93l6.269-1.125c1.104-.205 1.104-1.76 0-1.944l-6.269-1.126c-2.042-.368-3.625-1.944-4-3.929l-1.145-6.16c-.209-1.084-1.792-1.084-1.979 0z" fill="url(#paint0_linear_59_8353)"/><path d="M754.539 33.25l-.39 1.896c-.128.617-.673 1.095-1.36 1.209l-2.132.346c-.376.063-.376.541 0 .598l2.132.346c.694.114 1.232.599 1.36 1.21l.39 1.895c.07.333.609.333.672 0l.39-1.896c.128-.617.673-1.095 1.36-1.209l2.132-.346c.376-.063.376-.541 0-.598l-2.132-.346c-.694-.114-1.232-.599-1.36-1.21l-.39-1.895c-.07-.333-.609-.333-.672 0z" fill="url(#paint1_linear_59_8353)"/><path d="M855.611 98.933l-1.3 7.067c-.426 2.301-2.246 4.086-4.539 4.508l-7.117 1.291c-1.253.235-1.253 2.02 0 2.231l7.117 1.291a5.648 5.648 0 014.539 4.508l1.3 7.067c.237 1.245 2.034 1.245 2.246 0l1.301-7.067c.425-2.301 2.246-4.085 4.539-4.508l7.117-1.291c1.253-.235 1.253-2.019 0-2.231l-7.117-1.291a5.644 5.644 0 01-4.539-4.508l-1.301-7.067c-.236-1.244-2.033-1.244-2.246 0z" fill="url(#paint2_linear_59_8353)"/><path opacity=".6" d="M1007.12 63.905l-1.31 6.86c-.43 2.229-2.25 3.975-4.57 4.385l-7.155 1.262c-1.259.223-1.259 1.953 0 2.176l7.155 1.262c2.32.411 4.14 2.156 4.57 4.386l1.31 6.859c.24 1.207 2.04 1.207 2.27 0l1.32-6.86c.43-2.229 2.25-3.975 4.57-4.385l7.15-1.262c1.26-.223 1.26-1.953 0-2.176l-7.15-1.262c-2.32-.411-4.14-2.156-4.57-4.386l-1.32-6.859c-.23-1.207-2.03-1.207-2.27 0z" fill="url(#paint3_linear_59_8353)"/><path opacity=".6" d="M257.805 433.217l-1.771 9.225c-.575 2.998-3.022 5.347-6.148 5.897l-9.617 1.698c-1.692.299-1.692 2.627 0 2.926l9.617 1.698c3.126.552 5.575 2.899 6.148 5.897l1.771 9.225c.312 1.623 2.738 1.623 3.05 0l1.77-9.225c.576-2.998 3.022-5.347 6.148-5.897l9.617-1.698c1.693-.299 1.693-2.627 0-2.926l-9.617-1.698c-3.126-.552-5.574-2.899-6.148-5.897l-1.77-9.225c-.312-1.623-2.738-1.623-3.05 0z" fill="url(#paint4_linear_59_8353)"/><path opacity=".6" d="M701.162 404.593l-.827 4.494a3.58 3.58 0 01-2.873 2.873l-4.494.827c-.791.146-.791 1.28 0 1.426l4.494.827a3.58 3.58 0 012.873 2.873l.827 4.494c.146.791 1.28.791 1.426 0l.827-4.494a3.58 3.58 0 012.873-2.873l4.494-.827c.791-.146.791-1.28 0-1.426l-4.494-.827a3.58 3.58 0 01-2.873-2.873l-.827-4.494c-.146-.791-1.28-.791-1.426 0z" fill="url(#paint5_linear_59_8353)"/><path d="M6.276 219.375l-.53 2.843a2.284 2.284 0 01-1.845 1.814l-2.894.519c-.51.095-.51.813 0 .898l2.894.519c.942.17 1.673.897 1.846 1.814l.529 2.843c.096.5.826.5.913 0l.529-2.843a2.284 2.284 0 011.845-1.814l2.894-.519c.51-.095.51-.813 0-.898l-2.894-.519a2.284 2.284 0 01-1.845-1.814l-.53-2.843c-.095-.5-.826-.5-.912 0z" fill="url(#paint6_linear_59_8353)"/><path d="M302.539 243.25l-.39 1.896c-.128.617-.673 1.095-1.36 1.209l-2.132.346c-.376.063-.376.541 0 .598l2.132.346c.694.114 1.232.599 1.36 1.209l.39 1.896c.07.333.609.333.672 0l.39-1.896c.128-.617.673-1.095 1.36-1.209l2.132-.346c.376-.063.376-.541 0-.598l-2.132-.346c-.694-.114-1.232-.599-1.36-1.209l-.39-1.896c-.07-.333-.609-.333-.672 0z" fill="url(#paint7_linear_59_8353)"/><path d="M124.682 266.62l-.864 4.698a3.754 3.754 0 01-3.018 2.997l-4.731.859c-.833.156-.833 1.342 0 1.482l4.731.859a3.755 3.755 0 013.018 2.997l.864 4.698c.157.827 1.352.827 1.493 0l.865-4.698a3.754 3.754 0 013.017-2.997l4.731-.859c.833-.156.833-1.342 0-1.482l-4.731-.859a3.754 3.754 0 01-3.017-2.997l-.865-4.698c-.157-.827-1.351-.827-1.493 0z" fill="url(#paint8_linear_59_8353)"/><path opacity=".6" d="M358.161 423.593l-.862 4.494c-.281 1.461-1.472 2.605-2.995 2.873l-4.686.827a.72.72 0 000 1.426l4.686.827c1.523.269 2.715 1.412 2.995 2.873l.862 4.494c.152.791 1.334.791 1.486 0l.863-4.494c.28-1.461 1.472-2.605 2.995-2.873l4.685-.827a.72.72 0 000-1.426l-4.685-.827c-1.523-.269-2.716-1.412-2.995-2.873l-.863-4.494c-.152-.791-1.334-.791-1.486 0z" fill="url(#paint9_linear_59_8353)"/><defs><linearGradient id="paint0_linear_59_8353" x1="943.326" y1="19.743" x2="929.883" y2="6.06" gradientUnits="userSpaceOnUse"><stop stop-color="#DF4329" stop-opacity=".9"/><stop offset=".786" stop-color="#A686F2"/></linearGradient><linearGradient id="paint1_linear_59_8353" x1="757.202" y1="39.075" x2="753.094" y2="34.453" gradientUnits="userSpaceOnUse"><stop stop-color="#DF4329" stop-opacity=".9"/><stop offset=".786" stop-color="#A686F2"/></linearGradient><linearGradient id="paint2_linear_59_8353" x1="864.501" y1="120.651" x2="849.076" y2="105.118" gradientUnits="userSpaceOnUse"><stop offset=".33" stop-color="#10AEAE" stop-opacity=".61"/><stop offset=".43" stop-color="#1CB3B1" stop-opacity=".66"/><stop offset=".63" stop-color="#3BC0B9" stop-opacity=".79"/><stop offset=".91" stop-color="#6FD7C6"/></linearGradient><linearGradient id="paint3_linear_59_8353" x1="1016.06" y1="84.988" x2="1001.1" y2="69.391" gradientUnits="userSpaceOnUse"><stop offset=".236" stop-color="#A686F2"/><stop offset=".39" stop-color="#A382F0" stop-opacity=".978"/><stop offset=".533" stop-color="#9A77EB" stop-opacity=".958"/><stop offset=".67" stop-color="#8A65E3" stop-opacity=".938"/><stop offset=".804" stop-color="#744AD7" stop-opacity=".919"/><stop offset=".935" stop-color="#5829C8" stop-opacity=".9"/></linearGradient><linearGradient id="paint4_linear_59_8353" x1="269.827" y1="461.57" x2="249.709" y2="440.595" gradientUnits="userSpaceOnUse"><stop offset=".236" stop-color="#A686F2"/><stop offset=".39" stop-color="#A382F0" stop-opacity=".978"/><stop offset=".533" stop-color="#9A77EB" stop-opacity=".958"/><stop offset=".67" stop-color="#8A65E3" stop-opacity=".938"/><stop offset=".804" stop-color="#744AD7" stop-opacity=".919"/><stop offset=".935" stop-color="#5829C8" stop-opacity=".9"/></linearGradient><linearGradient id="paint5_linear_59_8353" x1="706.78" y1="418.406" x2="696.971" y2="408.596" gradientUnits="userSpaceOnUse"><stop offset=".236" stop-color="#A686F2"/><stop offset=".39" stop-color="#A382F0" stop-opacity=".978"/><stop offset=".533" stop-color="#9A77EB" stop-opacity=".958"/><stop offset=".67" stop-color="#8A65E3" stop-opacity=".938"/><stop offset=".804" stop-color="#744AD7" stop-opacity=".919"/><stop offset=".935" stop-color="#5829C8" stop-opacity=".9"/></linearGradient><linearGradient id="paint6_linear_59_8353" x1="9.89" y1="228.112" x2="3.686" y2="221.797" gradientUnits="userSpaceOnUse"><stop stop-color="#DF4329" stop-opacity=".9"/><stop offset=".786" stop-color="#A686F2"/></linearGradient><linearGradient id="paint7_linear_59_8353" x1="305.202" y1="249.075" x2="301.094" y2="244.453" gradientUnits="userSpaceOnUse"><stop stop-color="#DF4329" stop-opacity=".9"/><stop offset=".786" stop-color="#A686F2"/></linearGradient><linearGradient id="paint8_linear_59_8353" x1="130.592" y1="281.058" x2="120.338" y2="270.732" gradientUnits="userSpaceOnUse"><stop offset=".33" stop-color="#10AEAE" stop-opacity=".61"/><stop offset=".43" stop-color="#1CB3B1" stop-opacity=".66"/><stop offset=".63" stop-color="#3BC0B9" stop-opacity=".79"/><stop offset=".91" stop-color="#6FD7C6"/></linearGradient><linearGradient id="paint9_linear_59_8353" x1="364.018" y1="437.406" x2="354.217" y2="427.188" gradientUnits="userSpaceOnUse"><stop offset=".236" stop-color="#A686F2"/><stop offset=".39" stop-color="#A382F0" stop-opacity=".978"/><stop offset=".533" stop-color="#9A77EB" stop-opacity=".958"/><stop offset=".67" stop-color="#8A65E3" stop-opacity=".938"/><stop offset=".804" stop-color="#744AD7" stop-opacity=".919"/><stop offset=".935" stop-color="#5829C8" stop-opacity=".9"/></linearGradient></defs></svg> diff --git a/ee/app/assets/javascripts/pages/subscriptions/trials/duo_pro/new/index.js b/ee/app/assets/javascripts/pages/subscriptions/trials/duo_pro/new/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e965df9f0f7999b2db5d907048f8946e45511ac1 --- /dev/null +++ b/ee/app/assets/javascripts/pages/subscriptions/trials/duo_pro/new/index.js @@ -0,0 +1,8 @@ +import { initTrialCreateLeadForm } from 'ee/trials/init_create_lead_form'; +import { trackSaasTrialGroup } from 'ee/google_tag_manager'; +import 'ee/trials/track_trial_user_errors'; +import { initNamespaceSelector } from 'ee/trials/init_namespace_selector'; + +trackSaasTrialGroup(); +initTrialCreateLeadForm(); +initNamespaceSelector(); diff --git a/ee/app/assets/javascripts/trials/components/trial_create_lead_form.vue b/ee/app/assets/javascripts/trials/components/trial_create_lead_form.vue index 097c1db75ac70fc2375ce2d7536136c318d3aa84..50f9fb1484adb258ed22f84209f7edaf93c25c15 100644 --- a/ee/app/assets/javascripts/trials/components/trial_create_lead_form.vue +++ b/ee/app/assets/javascripts/trials/components/trial_create_lead_form.vue @@ -32,7 +32,7 @@ export default { directives: { autofocusonshow, }, - inject: ['user', 'submitPath'], + inject: ['user', 'formSubmitText', 'submitPath'], data() { return this.user; }, @@ -46,6 +46,9 @@ export default { ...companySizes, ]; }, + submitText() { + return this.formSubmitText || this.$options.i18n.formSubmitText; + }, }, methods: { onSubmit() { @@ -137,7 +140,7 @@ export default { /> </gl-form-group> <gl-button type="submit" variant="confirm" class="gl-w-20" data-testid="continue"> - {{ $options.i18n.formSubmitText }} + {{ submitText }} </gl-button> </gl-form> </template> diff --git a/ee/app/assets/javascripts/trials/init_create_lead_form.js b/ee/app/assets/javascripts/trials/init_create_lead_form.js index e4f01aafb07b26ec3d575ffbe1e560b1e3402919..d77484b09cc5e73b61f73e3bdb7b12a2cb658e14 100644 --- a/ee/app/assets/javascripts/trials/init_create_lead_form.js +++ b/ee/app/assets/javascripts/trials/init_create_lead_form.js @@ -18,6 +18,7 @@ export const initTrialCreateLeadForm = () => { country, state, phoneNumber, + formSubmitText, } = el.dataset; return new Vue({ @@ -34,6 +35,7 @@ export const initTrialCreateLeadForm = () => { phoneNumber, }, submitPath, + formSubmitText, }, render(createElement) { return createElement(TrialCreateLeadForm); diff --git a/ee/app/assets/stylesheets/pages/subscriptions.scss b/ee/app/assets/stylesheets/pages/subscriptions.scss index 6b8d761be97caf0d3536e4c2041bd6068b840d81..29e449285a6ae2830ed2c1a15de7eeb56ebdbd9c 100644 --- a/ee/app/assets/stylesheets/pages/subscriptions.scss +++ b/ee/app/assets/stylesheets/pages/subscriptions.scss @@ -201,3 +201,13 @@ $subscriptions-full-width-lg: 541px; } } } + +.duo-pro-trials { + background-color: $brand-charcoal; + + .bg-decorations { + background-image: url('illustrations/bg-decorations.svg'); + background-repeat: no-repeat; + background-position: top 40% right 70px; + } +} diff --git a/ee/app/controllers/subscriptions/trials/duo_pro_controller.rb b/ee/app/controllers/subscriptions/trials/duo_pro_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..337ac886f32bbd2032a6bc97b9d71934775bf290 --- /dev/null +++ b/ee/app/controllers/subscriptions/trials/duo_pro_controller.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# EE:SaaS +module Subscriptions + module Trials + class DuoProController < ApplicationController + include RegistrationsTracking + + layout 'minimal' + + skip_before_action :set_confirm_warning + before_action :check_feature_available! + before_action :authenticate_user! + + feature_category :purchase + urgency :low + + def new + if params[:step] == GitlabSubscriptions::Trials::CreateService::TRIAL + render :step_namespace + else + render :step_lead + end + end + + def create + # TODO: Implement actual duo pro trial activation + # https://gitlab.com/gitlab-org/gitlab/-/issues/435875 + redirect_to new_trials_duo_pro_path( + namespace_id: params[:namespace_id], + step: GitlabSubscriptions::Trials::CreateService::TRIAL + ) + end + + private + + def authenticate_user! + return if current_user + + redirect_to new_trial_registration_path(glm_tracking_params), alert: I18n.t('devise.failure.unauthenticated') + end + + def check_feature_available! + if Feature.enabled?(:duo_pro_trials, current_user, type: :wip) && + ::Gitlab::Saas.feature_available?(:subscriptions_trials) + return + end + + render_404 + end + end + end +end diff --git a/ee/app/helpers/trials_helper.rb b/ee/app/helpers/trials_helper.rb index 28c98540c65c2d14aa6aa2f4217dd312b16d7c6e..ed562d47965aece9692045bc88b96cab8e3861b8 100644 --- a/ee/app/helpers/trials_helper.rb +++ b/ee/app/helpers/trials_helper.rb @@ -4,20 +4,17 @@ module TrialsHelper TRIAL_ONBOARDING_SOURCE_URLS = %w[about.gitlab.com docs.gitlab.com learn.gitlab.com].freeze def create_lead_form_data - { + _lead_form_data.merge( submit_path: trials_path( step: GitlabSubscriptions::Trials::CreateService::LEAD, **params.permit(:namespace_id).merge(glm_params) - ), - first_name: current_user.first_name, - last_name: current_user.last_name, - company_name: current_user.organization - }.merge( - params.permit( - :first_name, :last_name, :company_name, :company_size, :phone_number, :country, :state - ).to_h.symbolize_keys + ) ) end + def create_duo_pro_lead_form_data + _lead_form_data.merge(submit_path: trials_duo_pro_path(namespace_id: params[:namespace_id])) + end + def create_company_form_data submit_params = glm_params.merge(passed_through_params.to_unsafe_h) { @@ -101,4 +98,16 @@ def trial_eligible_namespaces def any_trial_eligible_namespaces? trial_eligible_namespaces.any? end + + def _lead_form_data + { + first_name: current_user.first_name, + last_name: current_user.last_name, + company_name: current_user.organization + }.merge( + params.permit( + :first_name, :last_name, :company_name, :company_size, :phone_number, :country, :state + ).to_h.symbolize_keys + ) + end end diff --git a/ee/app/views/groups/billings/_free_plan_billing_index.html.haml b/ee/app/views/groups/billings/_free_plan_billing_index.html.haml index 31712364a9f4c4718c3893ac13d0a8424abc4058..8249f6c40beba8cc4caca395227231946c7a3d4b 100644 --- a/ee/app/views/groups/billings/_free_plan_billing_index.html.haml +++ b/ee/app/views/groups/billings/_free_plan_billing_index.html.haml @@ -13,8 +13,12 @@ = s_("BillingPlans|All plans have unlimited (private) repositories.") .gl-mb-5 = s_("BillingPlans|Ready to explore the value of the paid features today? Start a trial, no credit card required.") - = render Pajamas::ButtonComponent.new(href: new_trial_path(namespace_id: namespace.id), category: 'secondary', variant: 'confirm', button_options: { class: 'gl-mb-6', data: start_free_trial_data }) do - = s_("BillingPlans|Start a free Ultimate trial") + .gl-mb-6.gl-justify-content-center.gl-display-flex.gl-flex-wrap.gl-gap-3 + = render Pajamas::ButtonComponent.new(href: new_trial_path(namespace_id: namespace.id), category: 'secondary', variant: 'confirm', button_options: { data: start_free_trial_data }) do + = s_("BillingPlans|Start a free Ultimate trial") + - if Feature.enabled?(:duo_pro_trials, current_user, type: :wip) + = render Pajamas::ButtonComponent.new(href: new_trials_duo_pro_path(namespace_id: namespace.id), category: 'secondary', variant: 'confirm') do + = s_("BillingPlans|Start a free Duo Pro trial") .billing-plan-divider.gl-m-auto.gl-border-b.gl-mb-7 - image_alt = s_('InProductMarketing|Team members collaborating') = image_tag 'marketing/free-trial-team-members.png', alt: image_alt, title: image_alt, width: 280, height: 125, class: 'gl-mb-6' diff --git a/ee/app/views/subscriptions/trials/_lead_form.html.haml b/ee/app/views/subscriptions/trials/_lead_form.html.haml index 9b9c2f62839bc74121b68d037ee33d68f14cc91a..6a95694d3cbbb1e40a89a0125e3a90ab49a675b3 100644 --- a/ee/app/views/subscriptions/trials/_lead_form.html.haml +++ b/ee/app/views/subscriptions/trials/_lead_form.html.haml @@ -17,4 +17,4 @@ #js-trial-create-lead-form{ data: create_lead_form_data } .col-md-4.trial-illustration - = image_tag 'illustrations/saas-trial-illustration.svg', alt: '', class: 'gl-display-none d-md-inline gl-w-full' + = image_tag 'illustrations/saas-trial-illustration.svg', alt: '', class: 'gl-display-none gl-md-display-inline gl-w-full' diff --git a/ee/app/views/subscriptions/trials/duo_pro/_advantages_list.html.haml b/ee/app/views/subscriptions/trials/duo_pro/_advantages_list.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..e334e6a5c31e578fbb425514a74ee67e47e5e650 --- /dev/null +++ b/ee/app/views/subscriptions/trials/duo_pro/_advantages_list.html.haml @@ -0,0 +1,17 @@ +.gl-m-7.gl-text-white + .gl-display-flex.gl-align-items-center.gl-gap-5 + = sprite_icon('upgrade', size: 24) + %span + = s_('DuoProTrial|Accelerate coding') + .gl-display-flex.gl-align-items-center.gl-gap-5.gl-pt-6 + = sprite_icon('shield', size: 24) + %span + = s_('DuoProTrial|Keep your Source Code protected') + .gl-display-flex.gl-align-items-center.gl-gap-5.gl-pt-6 + = sprite_icon('code', size: 24) + %span + = s_('DuoProTrial|Billions of lines of code at your fingertips') + .gl-display-flex.gl-align-items-center.gl-gap-5.gl-pt-6 + = sprite_icon('earth', size: 24) + %span + = s_('DuoProTrial|Support in your language of choice') diff --git a/ee/app/views/subscriptions/trials/duo_pro/_lead_form.html.haml b/ee/app/views/subscriptions/trials/duo_pro/_lead_form.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..6085f2d6ea5d5cd837f16227a915d8cbb36767d5 --- /dev/null +++ b/ee/app/views/subscriptions/trials/duo_pro/_lead_form.html.haml @@ -0,0 +1,19 @@ +- @body_class = 'duo-pro-trials' +- page_title s_('DuoProTrial|Start your free Duo Pro trial') + +.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-align-items-center.gl-justify-content-center.bg-decorations + .m-sm-6 + .gl-p-7.gl-rounded-lg.gl-bg-white + = sprite_icon('tanuki-ai', size: 32, css_class: 'gl-pb-3') + + %h2.gl-pb-3.gl-my-0 + = s_('DuoProTrial|Start your free Duo Pro trial') + + %p.gl-text-gray-700.gl-font-lg + = s_('DuoProTrial|We just need some additional information to activate your trial.') + + = yield :before_form + + #js-trial-create-lead-form{ data: create_duo_pro_lead_form_data } + + = render 'advantages_list' diff --git a/ee/app/views/subscriptions/trials/duo_pro/_select_namespace_form.html.haml b/ee/app/views/subscriptions/trials/duo_pro/_select_namespace_form.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..289bc03765588254fce41a545464cdc43b0e28f6 --- /dev/null +++ b/ee/app/views/subscriptions/trials/duo_pro/_select_namespace_form.html.haml @@ -0,0 +1,32 @@ +- @body_class = 'duo-pro-trials' +- page_title s_('DuoProTrial|Start your free Duo Pro trial') + +.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-align-items-center.gl-justify-content-center.gl-py-6.bg-decorations + .m-sm-6.gl-max-w-62 + .gl-p-7.gl-rounded-lg.gl-bg-white + = sprite_icon('tanuki-ai', size: 32, css_class: 'gl-pb-3') + + %h2.gl-pb-5.gl-my-0 + - if any_trial_eligible_namespaces? + = s_('DuoProTrial|Apply your Duo Pro trial to a new or existing group') + - else + = s_('DuoProTrial|Create a group to start your Duo Pro trial') + + = yield :before_form + + = gitlab_ui_form_for '', url: trials_duo_pro_path do |f| + .js-namespace-selector{ data: namespace_selector_data({}) } + - if should_ask_company_question? + .form-group + = f.label :trial_entity, _('Who will be using GitLab?') + %div + .form-check-inline + = f.gitlab_ui_radio_component :trial_entity, 'company', _('My company or team'), + radio_options: { required: true, checked: params[:trial_entity] == 'company' } + .form-check-inline + = f.gitlab_ui_radio_component :trial_entity, 'individual', _('Just me'), + radio_options: { required: true, checked: params[:trial_entity] == 'individual' } + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do + = s_('DuoProTrial|Activate my trial') + + = render 'advantages_list' diff --git a/ee/app/views/subscriptions/trials/duo_pro/step_lead.html.haml b/ee/app/views/subscriptions/trials/duo_pro/step_lead.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..0fb563fd00e8b841797c159b1ab1fdb646b747b1 --- /dev/null +++ b/ee/app/views/subscriptions/trials/duo_pro/step_lead.html.haml @@ -0,0 +1 @@ += render 'lead_form' diff --git a/ee/app/views/subscriptions/trials/duo_pro/step_namespace.html.haml b/ee/app/views/subscriptions/trials/duo_pro/step_namespace.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..a590e02459c225b89990db69293d3ce16867386f --- /dev/null +++ b/ee/app/views/subscriptions/trials/duo_pro/step_namespace.html.haml @@ -0,0 +1 @@ += render 'select_namespace_form' diff --git a/ee/config/feature_flags/wip/duo_pro_trials.yml b/ee/config/feature_flags/wip/duo_pro_trials.yml new file mode 100644 index 0000000000000000000000000000000000000000..2c2a64d20b35d4d1df3dbc3dcd0a1f9e72941efb --- /dev/null +++ b/ee/config/feature_flags/wip/duo_pro_trials.yml @@ -0,0 +1,9 @@ +--- +name: duo_pro_trials +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432302 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141597 +rollout_issue_url: +milestone: '16.9' +group: group::acquisition +type: wip +default_enabled: false diff --git a/ee/config/routes/subscription.rb b/ee/config/routes/subscription.rb index 462369bc94c0765891006fa32d9dd16ede7dcf2d..1e91d34ad2771e7bb61ae2df6bf01798c7a0e7b5 100644 --- a/ee/config/routes/subscription.rb +++ b/ee/config/routes/subscription.rb @@ -15,4 +15,8 @@ scope module: :subscriptions do resources :trials, only: [:new, :create] + + namespace :trials do + resource :duo_pro, only: [:new, :create] + end end diff --git a/ee/spec/frontend/trials/components/trial_create_lead_form_spec.js b/ee/spec/frontend/trials/components/trial_create_lead_form_spec.js index dac57a2eb21e44ffc7f70471af3ceac3aaae307c..7eada425d703241b23f89d7b4b06f9405e3cc969 100644 --- a/ee/spec/frontend/trials/components/trial_create_lead_form_spec.js +++ b/ee/spec/frontend/trials/components/trial_create_lead_form_spec.js @@ -18,9 +18,10 @@ Vue.use(VueApollo); describe('TrialCreateLeadForm', () => { let wrapper; - const createComponent = ({ mountFunction = shallowMountExtended } = {}) => { + const createComponent = ({ mountFunction = shallowMountExtended, formSubmitText = '' } = {}) => { return mountFunction(TrialCreateLeadForm, { provide: { + formSubmitText, submitPath: SUBMIT_PATH, user: FORM_DATA, }, @@ -41,10 +42,6 @@ describe('TrialCreateLeadForm', () => { wrapper = createComponent(); }); - it('has the "Continue" text on the submit button', () => { - expect(findButton().text()).toBe(TRIAL_FORM_SUBMIT_TEXT); - }); - it.each` testid | value ${'first-name'} | ${'Joe'} @@ -69,6 +66,23 @@ describe('TrialCreateLeadForm', () => { }); }); + describe('submit button text', () => { + it('has the "Continue" text on the submit button', () => { + wrapper = createComponent(); + + expect(findButton().text()).toBe(TRIAL_FORM_SUBMIT_TEXT); + }); + + describe('when submit button text is provided', () => { + it('has the provided text on the submit button', () => { + const formSubmitText = '_formSubmitText_'; + wrapper = createComponent({ formSubmitText }); + + expect(findButton().text()).toBe(formSubmitText); + }); + }); + }); + describe('submitting', () => { beforeEach(() => { wrapper = createComponent({ mountFunction: mountExtended }); diff --git a/ee/spec/helpers/trials_helper_spec.rb b/ee/spec/helpers/trials_helper_spec.rb index a7c953bdff384832d38cfbbfd911f3f9ff14e05a..3c467a33d9a8439bc7ff7629c6424386aec77de7 100644 --- a/ee/spec/helpers/trials_helper_spec.rb +++ b/ee/spec/helpers/trials_helper_spec.rb @@ -62,6 +62,61 @@ end end + describe '#create_duo_pro_lead_form_data' do + let(:user) { build_stubbed(:user, user_detail: build_stubbed(:user_detail, organization: '_org_')) } + + let(:extra_params) do + { + first_name: '_params_first_name_', + last_name: '_params_last_name_', + company_name: '_params_company_name_', + company_size: '_company_size_', + phone_number: '1234', + country: '_country_', + state: '_state_' + } + end + + let(:params) { ActionController::Parameters.new(extra_params) } + + before do + allow(helper).to receive(:params).and_return(params) + allow(helper).to receive(:current_user).and_return(user) + end + + it 'provides expected form data' do + keys = extra_params.keys + [:submit_path] + + expect(helper.create_duo_pro_lead_form_data.keys.map(&:to_sym)).to match_array(keys) + end + + it 'allows overriding data with params' do + expect(helper.create_duo_pro_lead_form_data).to match(a_hash_including(extra_params)) + end + + context 'when namespace_id is in the params' do + let(:extra_params) { { namespace_id: non_existing_record_id } } + + it 'provides the submit path with the namespace_id' do + expect(helper.create_duo_pro_lead_form_data[:submit_path]).to eq(trials_duo_pro_path(**params.permit!)) + end + end + + context 'when params are empty' do + let(:extra_params) { {} } + + it 'uses the values from current user' do + current_user_attributes = { + first_name: user.first_name, + last_name: user.last_name, + company_name: user.organization + } + + expect(helper.create_duo_pro_lead_form_data).to match(a_hash_including(current_user_attributes)) + end + end + end + describe '#create_company_form_data' do let(:user) { build_stubbed(:user) } let(:extra_params) do diff --git a/ee/spec/requests/subscriptions/trials/duo_pro_controller_spec.rb b/ee/spec/requests/subscriptions/trials/duo_pro_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1f01715bdf6f50606625511488724f4b5236b056 --- /dev/null +++ b/ee/spec/requests/subscriptions/trials/duo_pro_controller_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Subscriptions::Trials::DuoProController, feature_category: :purchase do + let_it_be(:user, reload: true) { create(:user) } + + let(:duo_pro_trials_feature_flag) { true } + let(:subscriptions_trials_saas_feature) { true } + + before do + stub_feature_flags(duo_pro_trials: duo_pro_trials_feature_flag) + + stub_saas_features( + subscriptions_trials: subscriptions_trials_saas_feature, + marketing_google_tag_manager: false + ) + end + + describe 'GET new' do + let(:base_params) { {} } + + subject(:get_new) do + get new_trials_duo_pro_path, params: base_params + response + end + + context 'when not authenticated' do + it { is_expected.to redirect_to_trial_registration } + end + + context 'when authenticated' do + before do + login_as(user) + end + + it { is_expected.to render_lead_form } + + context 'when duo_pro_trials feature flag is disabled' do + let(:duo_pro_trials_feature_flag) { false } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when subscriptions_trials saas feature is not available' do + let(:subscriptions_trials_saas_feature) { false } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when on the trial step' do + let(:base_params) { { step: 'trial' } } + + it { is_expected.to render_select_namespace } + end + end + end + + describe 'POST create' do + subject(:post_create) do + post trials_duo_pro_path, params: {} + response + end + + context 'when not authenticated' do + it 'redirects to trial registration' do + expect(post_create).to redirect_to_trial_registration + end + end + + context 'when authenticated' do + before do + login_as(user) + end + + context 'when successful' do + it 'redirects to new path' do + expect(post_create).to redirect_to(new_trials_duo_pro_path( + step: GitlabSubscriptions::Trials::CreateService::TRIAL + )) + end + end + + context 'when duo_pro_trials feature flag is disabled' do + let(:duo_pro_trials_feature_flag) { false } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'when subscriptions_trials saas feature is not available' do + let(:subscriptions_trials_saas_feature) { false } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + end + end + + RSpec::Matchers.define :render_lead_form do + match do |response| + expect(response).to have_gitlab_http_status(:ok) + + expect(response.body).to include(s_('DuoProTrial|Start your free Duo Pro trial')) + + expect(response.body).to include( + s_('DuoProTrial|We just need some additional information to activate your trial.') + ) + end + end + + RSpec::Matchers.define :render_select_namespace do + match do |response| + expect(response).to have_gitlab_http_status(:ok) + + expect(response.body).to include(s_('DuoProTrial|Create a group to start your Duo Pro trial')) + end + end + + RSpec::Matchers.define :redirect_to_trial_registration do + match do |response| + expect(response).to redirect_to(new_trial_registration_path) + expect(flash[:alert]).to include('You need to sign in or sign up before continuing') + end + end +end diff --git a/ee/spec/views/groups/billings/index.html.haml_spec.rb b/ee/spec/views/groups/billings/index.html.haml_spec.rb index 97c90110e9cb43131c1ae9698faa5051f2b2e538..cf77da30dc31098440fdb7b1efb065b388d1e54d 100644 --- a/ee/spec/views/groups/billings/index.html.haml_spec.rb +++ b/ee/spec/views/groups/billings/index.html.haml_spec.rb @@ -67,6 +67,32 @@ def expect_to_have_tracking(action:, label: nil) expect(rendered).to have_css(css) end + context 'with Duo Pro trial link' do + it 'renders the link' do + render + + expect(rendered).to have_link( + 'Start a free Duo Pro trial', + href: new_trials_duo_pro_path(namespace_id: group.id) + ) + end + + context 'when duo_pro_trials is disabled' do + before do + stub_feature_flags(duo_pro_trials: false) + end + + it 'does not render the link' do + render + + expect(rendered).not_to have_link( + 'Start a free Duo Pro trial', + href: new_trials_duo_pro_path(namespace_id: group.id) + ) + end + end + end + context 'with an expired trial' do let_it_be(:group) { create(:group_with_plan, plan: :free_plan, trial_ends_on: Date.yesterday) } diff --git a/ee/spec/views/subscriptions/trials/duo_pro/_advantages_list.html.haml_spec.rb b/ee/spec/views/subscriptions/trials/duo_pro/_advantages_list.html.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..924643b2bbfc8cf6a17567cd559f00283a58d372 --- /dev/null +++ b/ee/spec/views/subscriptions/trials/duo_pro/_advantages_list.html.haml_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'subscriptions/trials/duo_pro/_advantages_list.html.haml', feature_category: :purchase do + it 'renders the list' do + render 'subscriptions/trials/duo_pro/advantages_list' + + expect(rendered).to have_content(s_('DuoProTrial|Accelerate coding')) + expect(rendered).to have_content(s_('DuoProTrial|Keep your Source Code protected')) + expect(rendered).to have_content(s_('DuoProTrial|Billions of lines of code at your fingertips')) + expect(rendered).to have_content(s_('DuoProTrial|Support in your language of choice')) + end +end diff --git a/ee/spec/views/subscriptions/trials/duo_pro/_lead_form.html.haml_spec.rb.haml_spec.rb b/ee/spec/views/subscriptions/trials/duo_pro/_lead_form.html.haml_spec.rb.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1abd98212fdc8031d11c0073920a06c5d1c06706 --- /dev/null +++ b/ee/spec/views/subscriptions/trials/duo_pro/_lead_form.html.haml_spec.rb.haml_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'subscriptions/trials/duo_pro/_lead_form.html.haml', feature_category: :purchase do + let_it_be(:user) { build_stubbed(:user) } + + before do + allow(view).to receive(:current_user) { user } + end + + it 'renders lead form' do + render 'subscriptions/trials/duo_pro/lead_form' + + expect(rendered).to have_content(s_('DuoProTrial|Start your free Duo Pro trial')) + expect(rendered).to have_content(s_('DuoProTrial|We just need some additional information to activate your trial.')) + expect(rendered).to render_template('subscriptions/trials/duo_pro/_advantages_list') + end +end diff --git a/ee/spec/views/subscriptions/trials/duo_pro/_select_namespace_form.html.haml_spec.rb b/ee/spec/views/subscriptions/trials/duo_pro/_select_namespace_form.html.haml_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ef3827054b39f39a75b376e3b598e18c49d533ae --- /dev/null +++ b/ee/spec/views/subscriptions/trials/duo_pro/_select_namespace_form.html.haml_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'subscriptions/trials/duo_pro/_select_namespace_form.html.haml', feature_category: :purchase do + let_it_be(:user) { build_stubbed(:user) } + + before do + allow(view).to receive(:current_user) { user } + end + + it 'renders select namespace form' do + render 'subscriptions/trials/duo_pro/select_namespace_form' + + expect(rendered).to have_content(s_('DuoProTrial|Create a group to start your Duo Pro trial')) + expect(rendered).to have_content(_('Who will be using GitLab?')) + expect(rendered).to have_content(_('My company or team')) + expect(rendered).to have_content(_('Just me')) + + expect(rendered).to render_template('subscriptions/trials/duo_pro/_advantages_list') + end + + context 'when there is trial eligible namespace' do + let_it_be(:group) { build_stubbed(:group) } + + before do + allow(user).to receive(:manageable_namespaces_eligible_for_trial).and_return([group]) + end + + it 'renders correct title' do + render 'subscriptions/trials/duo_pro/select_namespace_form' + + expect(rendered).to have_content(s_('DuoProTrial|Apply your Duo Pro trial to a new or existing group')) + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2432d343ec363cda291cba338708cc91015c1594..9c5d9239db5927c88bebe831564d136310717001 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7950,6 +7950,9 @@ msgstr "" msgid "BillingPlans|Spans the DevOps lifecycle" msgstr "" +msgid "BillingPlans|Start a free Duo Pro trial" +msgstr "" + msgid "BillingPlans|Start a free Ultimate trial" msgstr "" @@ -18344,6 +18347,33 @@ msgstr "" msgid "Due to inactivity, this project is scheduled to be deleted on %{deletion_date}. %{link_start}Why is this scheduled?%{link_end}" msgstr "" +msgid "DuoProTrial|Accelerate coding" +msgstr "" + +msgid "DuoProTrial|Activate my trial" +msgstr "" + +msgid "DuoProTrial|Apply your Duo Pro trial to a new or existing group" +msgstr "" + +msgid "DuoProTrial|Billions of lines of code at your fingertips" +msgstr "" + +msgid "DuoProTrial|Create a group to start your Duo Pro trial" +msgstr "" + +msgid "DuoProTrial|Keep your Source Code protected" +msgstr "" + +msgid "DuoProTrial|Start your free Duo Pro trial" +msgstr "" + +msgid "DuoProTrial|Support in your language of choice" +msgstr "" + +msgid "DuoProTrial|We just need some additional information to activate your trial." +msgstr "" + msgid "Duplicate page: A page with that title already exists" msgstr ""