Commit d9ddef5b authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '346920-eng-add-verify-your-identity-flow-to-first-mile' into 'master'

Add verification page during sign up

See merge request gitlab-org/gitlab!75646
parents e6cb3266 6f36e481
......@@ -72,6 +72,7 @@ Rails.application.routes.draw do
resources :groups_projects, only: [:new, :create] do
post :import, on: :collection
end
draw :verification
end
end
......
<script>
import { GlAlert, GlLoadingIcon, GlModal, GlSprintf } from '@gitlab/ui';
import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import { GlModal, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import Zuora from './zuora.vue';
const IFRAME_QUERY = Object.freeze({
enable_submit: false,
user_id: null,
});
// 350 is the mininum required height to get all iframe inputs visible
const IFRAME_MINIMUM_HEIGHT = 350;
const i18n = Object.freeze({
title: s__('Billings|Validate user account'),
description: s__(`
Billings|To use free pipeline minutes on shared runners, you’ll need to validate your account with a credit or debit card. This is required to discourage and reduce abuse on GitLab infrastructure.
%{strongStart}GitLab will not charge your card, it will only be used for validation.%{strongEnd}`),
iframeNotSupported: __('Your browser does not support iFrames'),
actions: {
primary: {
text: s__('Billings|Validate account'),
......@@ -24,10 +18,9 @@ Billings|To use free pipeline minutes on shared runners, you’ll need to valida
export default {
components: {
GlAlert,
GlLoadingIcon,
GlModal,
GlSprintf,
Zuora,
},
props: {
iframeUrl: {
......@@ -39,84 +32,21 @@ export default {
required: true,
},
},
data() {
return {
error: null,
isLoading: true,
isAlertDismissed: true,
};
},
computed: {
iframeSrc() {
const query = { ...IFRAME_QUERY, user_id: gon.current_user_id };
return `${this.iframeUrl}?${objectToQuery(query)}`;
},
iframeHeight() {
return IFRAME_MINIMUM_HEIGHT * window.devicePixelRatio;
},
shouldShowAlert() {
return this.error && !this.isAlertDismissed;
},
},
destroyed() {
window.removeEventListener('message', this.handleFrameMessages, true);
},
methods: {
submit(e) {
e.preventDefault();
this.error = null;
this.isLoading = true;
this.isAlertDismissed = true;
this.$refs.zuora.contentWindow.postMessage('submit', this.allowedOrigin);
this.$refs.zuora.submit();
},
show() {
this.isLoading = true;
this.$refs.modal.show();
},
hide() {
this.error = null;
this.$refs.modal.hide();
},
handleFrameLoaded() {
this.isLoading = false;
window.addEventListener('message', this.handleFrameMessages, true);
},
handleFrameMessages(event) {
if (!this.isEventAllowedForOrigin(event)) {
return;
}
if (event.data.success) {
this.$emit('success');
} else if (parseInt(event.data.code, 10) > 6) {
// 0-6 error codes mean client-side validation error after submit,
// no needs to reload the iframe and emit the failure event
this.error = event.data.msg;
this.isAlertDismissed = false;
window.removeEventListener('message', this.handleFrameMessages, true);
this.$refs.zuora.src = this.iframeSrc;
this.$emit('failure', { msg: this.error });
}
this.isLoading = false;
},
isEventAllowedForOrigin(event) {
try {
const url = new URL(event.origin);
return url.origin === this.allowedOrigin;
} catch {
return false;
}
},
handleAlertDismiss() {
this.isAlertDismissed = true;
},
},
i18n,
iframeHeight: IFRAME_MINIMUM_HEIGHT,
};
</script>
......@@ -135,22 +65,11 @@ export default {
>
</gl-sprintf>
</p>
<gl-alert v-if="shouldShowAlert" variant="danger" @dismiss="handleAlertDismiss">{{
error
}}</gl-alert>
<gl-loading-icon v-if="isLoading" size="lg" />
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<iframe
<zuora
ref="zuora"
:src="iframeSrc"
style="border: none"
width="100%"
:height="iframeHeight"
@load="handleFrameLoaded"
>
<p>{{ $options.i18n.iframeNotSupported }}</p>
</iframe>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
:initial-height="$options.iframeHeight"
:iframe-url="iframeUrl"
:allowed-origin="allowedOrigin"
/>
</gl-modal>
</template>
<script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
import { objectToQuery } from '~/lib/utils/url_utility';
const ZUORA_CLIENT_ERROR_HEIGHT = 15;
const IFRAME_QUERY = Object.freeze({
enable_submit: false,
user_id: null,
});
const I18N = {
iframeNotSupported: __('Your browser does not support iFrames'),
};
export default {
components: {
GlLoadingIcon,
GlAlert,
},
props: {
iframeUrl: {
type: String,
required: true,
},
allowedOrigin: {
type: String,
required: true,
},
initialHeight: {
type: Number,
required: true,
},
},
data() {
return {
error: null,
isLoading: true,
iframeHeight: this.initialHeight,
};
},
computed: {
iframeSrc() {
const query = { ...IFRAME_QUERY, user_id: gon.current_user_id };
return `${this.iframeUrl}?${objectToQuery(query)}`;
},
},
destroyed() {
window.removeEventListener('message', this.handleFrameMessages, true);
},
methods: {
handleFrameLoaded() {
this.isLoading = false;
window.addEventListener('message', this.handleFrameMessages, true);
},
submit() {
this.error = null;
this.isLoading = true;
this.iframeHeight = this.initialHeight;
this.$refs.zuora.contentWindow.postMessage('submit', this.allowedOrigin);
},
handleFrameMessages(event) {
if (!this.isEventAllowedForOrigin(event)) {
return;
}
if (event.data.success) {
this.$emit('success');
} else if (parseInt(event.data.code, 10) < 7) {
// 0-6 error codes mean client-side validation error after submit,
// no need to reload the iframe and emit the failure event
// Add a 15px height to the iframe to accomodate the error message
this.iframeHeight += ZUORA_CLIENT_ERROR_HEIGHT;
} else if (parseInt(event.data.code, 10) > 6) {
this.error = event.data.msg;
window.removeEventListener('message', this.handleFrameMessages, true);
this.$refs.zuora.src = this.iframeSrc;
this.$emit('failure', { msg: this.error });
}
this.isLoading = false;
},
isEventAllowedForOrigin(event) {
try {
const url = new URL(event.origin);
return url.origin === this.allowedOrigin;
} catch {
return false;
}
},
},
i18n: I18N,
};
</script>
<template>
<div>
<gl-alert v-if="error" variant="danger" @dismiss="error = null">
{{ error }}
</gl-alert>
<gl-loading-icon v-if="isLoading" size="lg" />
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<iframe
ref="zuora"
:src="iframeSrc"
style="border: none"
width="100%"
:height="iframeHeight"
@load="handleFrameLoaded"
>
<p>{{ $options.i18n.iframeNotSupported }}</p>
</iframe>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
</div>
</template>
import initVerification from 'ee/registrations/verification';
initVerification();
<script>
import { GlButton, GlPopover, GlLink, GlIcon } from '@gitlab/ui';
import Tracking from '~/tracking';
import { redirectTo } from '~/lib/utils/url_utility';
import Zuora from 'ee/billings/components/zuora.vue';
import {
I18N,
IFRAME_MINIMUM_HEIGHT,
EVENT_LABEL,
MOUNTED_EVENT,
SKIPPED_EVENT,
VERIFIED_EVENT,
} from '../constants';
export default {
components: {
GlButton,
GlPopover,
GlLink,
GlIcon,
Zuora,
},
mixins: [Tracking.mixin({ label: EVENT_LABEL })],
inject: ['nextStepUrl'],
data() {
return {
isSkipConfirmationVisible: false,
isSkipConfirmationDismissed: false,
iframeUrl: gon.registration_validation_form_url,
allowedOrigin: gon.subscriptions_url,
};
},
mounted() {
this.track(MOUNTED_EVENT);
},
methods: {
submit() {
this.$refs.zuora.submit();
},
handleSkip() {
if (this.isSkipConfirmationDismissed) {
this.skip();
} else {
this.isSkipConfirmationVisible = true;
}
},
dismissSkipConfirmation() {
this.isSkipConfirmationVisible = false;
this.isSkipConfirmationDismissed = true;
},
skip() {
this.track(SKIPPED_EVENT);
this.nextStep();
},
verified() {
this.track(VERIFIED_EVENT);
this.nextStep();
},
nextStep() {
redirectTo(this.nextStepUrl);
},
},
i18n: I18N,
iframeHeight: IFRAME_MINIMUM_HEIGHT,
};
</script>
<template>
<div
class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-px-5 gl-text-center"
>
<div class="verify-identity gl-display-flex gl-flex-direction-column gl-align-items-center">
<h2>
{{ $options.i18n.title }}
</h2>
<p>
{{ $options.i18n.description }}
</p>
<div
class="gl-border-gray-50 gl-border-solid gl-border-1 gl-rounded-base gl-w-85p gl-xs-w-full gl-px-4 gl-pt-3 gl-pb-4 gl-text-left"
>
<zuora
ref="zuora"
:initial-height="$options.iframeHeight"
:iframe-url="iframeUrl"
:allowed-origin="allowedOrigin"
@success="verified"
/>
<div class="gl-display-flex gl-mx-5 gl-mb-5">
<gl-icon name="information-o" :size="12" class="gl-mt-1" />
<div class="gl-ml-3 gl-text-secondary gl-font-sm">
{{ $options.i18n.disclaimer }}
</div>
</div>
<gl-button
ref="submitButton"
variant="confirm"
type="submit"
class="gl-w-full!"
@click="submit"
>
{{ $options.i18n.submit }}
</gl-button>
</div>
</div>
<div class="gl-mt-6 gl-md-mt-11!">
<gl-button ref="skipLink" variant="link" @click="handleSkip">
{{ $options.i18n.skip }}
</gl-button>
<gl-popover
v-if="isSkipConfirmationVisible"
ref="popover"
show
triggers="manual blur"
placement="top"
:target="$refs.skipLink"
@hide="dismissSkipConfirmation"
>
<template #title>
<div class="gl-display-flex gl-align-items-center">
<div class="gl-white-space-nowrap">{{ $options.i18n.skip_confirmation.title }}</div>
<gl-button
ref="popoverClose"
category="tertiary"
class="gl-opacity-10"
icon="close"
:aria-label="__('Close')"
@click="dismissSkipConfirmation"
/>
</div>
</template>
{{ $options.i18n.skip_confirmation.content }}
<div class="gl-text-right gl-mt-4">
<gl-link ref="skipConfirmationLink" @click="skip">
{{ $options.i18n.skip_confirmation.link }}
</gl-link>
</div>
</gl-popover>
<div class="gl-text-secondary gl-font-sm gl-mt-2">
{{ $options.i18n.skip_explanation }}
</div>
</div>
</div>
</template>
import { s__ } from '~/locale';
export const I18N = {
title: s__('RegistrationVerification|Enable free CI/CD minutes'),
description: s__(
"RegistrationVerification|To keep GitLab spam and abuse free we ask that you verify your identity with a valid payment method, such as a debit or credit card. Until then, you can't use free CI/CD minutes to build your application.",
),
disclaimer: s__(
'RegistrationVerification|GitLab will not charge your card, it will only be used for validation.',
),
submit: s__('RegistrationVerification|Validate account'),
skip: s__('RegistrationVerification|Skip this for now'),
skip_explanation: s__(
'RegistrationVerification|You can alway verify your account at a later time.',
),
skip_confirmation: {
title: s__('RegistrationVerification|Are you sure you want to skip this step?'),
content: s__(
'RegistrationVerification|Pipelines using shared GitLab runners will fail until you validate your account.',
),
link: s__("RegistrationVerification|Yes, I'd like to skip"),
},
};
export const IFRAME_MINIMUM_HEIGHT = 312;
export const EVENT_LABEL = 'registration_verification';
export const MOUNTED_EVENT = 'shown';
export const SKIPPED_EVENT = 'skipped';
export const VERIFIED_EVENT = 'verified';
import Vue from 'vue';
import Verification from './components/verification.vue';
export default () => {
const el = document.querySelector('.js-registration-verification');
if (!el) {
return false;
}
const { nextStepUrl } = el.dataset;
return new Vue({
el,
provide: { nextStepUrl },
render(createElement) {
return createElement(Verification);
},
});
};
......@@ -170,7 +170,8 @@ $subscriptions-full-width-lg: 541px;
.edit-group,
.edit-profile,
.new-project {
.new-project,
.verify-identity {
max-width: 460px;
.bar {
......
......@@ -35,18 +35,21 @@ module Registrations
if @project.saved?
combined_registration_experiment.track(:create_project, namespace: @project.namespace)
learn_gitlab_project = create_learn_gitlab_project
@learn_gitlab_project = create_learn_gitlab_project
if helpers.in_trial_onboarding_flow?
store_location
if helpers.registration_verification_enabled?
redirect_to new_users_sign_up_verification_path(url_params.merge(combined: true))
elsif helpers.in_trial_onboarding_flow?
record_experiment_user(:remove_known_trial_form_fields_welcoming, namespace_id: @group.id)
record_experiment_conversion_event(:remove_known_trial_form_fields_welcoming)
redirect_to trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: learn_gitlab_project.id)
redirect_to trial_getting_started_users_sign_up_welcome_path(url_params)
else
success_url = continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: @project.id)
success_url = continuous_onboarding_getting_started_users_sign_up_welcome_path(url_params)
if current_user.setup_for_company
store_location_for(:user, success_url)
success_url = new_trial_path
end
......@@ -93,5 +96,20 @@ module Registrations
modifed_group_params
end
def store_location
if current_user.setup_for_company && !helpers.in_trial_onboarding_flow?
success_url = continuous_onboarding_getting_started_users_sign_up_welcome_path(url_params)
store_location_for(:user, success_url)
end
end
def url_params
if helpers.in_trial_onboarding_flow?
{ learn_gitlab_project_id: @learn_gitlab_project.id }
else
{ project_id: @project.id }
end
end
end
end
......@@ -26,15 +26,17 @@ module Registrations
if @project.saved?
experiment(:combined_registration, user: current_user).track(:create_project, namespace: @project.namespace)
learn_gitlab_project = create_learn_gitlab_project
@learn_gitlab_project = create_learn_gitlab_project
experiment(:force_company_trial, user: current_user)
.track(:create_project, namespace: @project.namespace, project: @project, user: current_user)
if helpers.in_trial_onboarding_flow?
redirect_to trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: learn_gitlab_project.id)
if helpers.registration_verification_enabled?
redirect_to new_users_sign_up_verification_path(url_params)
elsif helpers.in_trial_onboarding_flow?
redirect_to trial_getting_started_users_sign_up_welcome_path(url_params)
else
redirect_to continuous_onboarding_getting_started_users_sign_up_welcome_path(project_id: @project.id)
redirect_to continuous_onboarding_getting_started_users_sign_up_welcome_path(url_params)
end
else
render :new
......@@ -50,5 +52,13 @@ module Registrations
def set_namespace
@namespace = Namespace.find_by_id(params[:namespace_id])
end
def url_params
if helpers.in_trial_onboarding_flow?
{ learn_gitlab_project_id: @learn_gitlab_project.id }
else
{ project_id: @project.id }
end
end
end
end
# frozen_string_literal: true
module Registrations
class VerificationController < ApplicationController
layout 'minimal'
before_action :check_if_gl_com_or_dev
feature_category :onboarding
before_action :publish_experiment
def new
end
private
def publish_experiment
experiment(:registration_verification, user: current_user).publish_to_client
end
end
end
......@@ -16,6 +16,28 @@ module EE
options.to_a.shuffle.append(other).map { |option| option.reverse }
end
def registration_verification_enabled?
experiment(:registration_verification, user: current_user) do |e|
e.candidate { true }
e.publish_to_database
e.run
end
end
def registration_verification_data
url = if params[:learn_gitlab_project_id].present?
trial_getting_started_users_sign_up_welcome_path(params.slice(:learn_gitlab_project_id).permit!)
elsif params[:combined].present? && current_user.setup_for_company
new_trial_path
elsif params[:project_id].present?
continuous_onboarding_getting_started_users_sign_up_welcome_path(params.slice(:project_id).permit!)
else
root_path
end
{ next_step_url: url }
end
private
def redirect_path
......
- @html_class = 'subscriptions-layout-html'
- page_title s_('RegistrationVerification|Verify your identity')
.js-registration-verification{ data: registration_verification_data }
---
name: registration_verification
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75646
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346927
milestone: '14.6'
type: experiment
group: group::activation
default_enabled: false
# frozen_string_literal: true
resources :verification, only: :new
......@@ -12,8 +12,9 @@ module EE
gon.roadmap_epics_limit = 1000
if ::Gitlab.dev_env_or_com?
gon.subscriptions_url = ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL
gon.payment_form_url = ::Gitlab::SubscriptionPortal::PAYMENT_FORM_URL
gon.subscriptions_url = ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL
gon.payment_form_url = ::Gitlab::SubscriptionPortal::PAYMENT_FORM_URL
gon.registration_validation_form_url = ::Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL
end
end
......
......@@ -40,6 +40,7 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do
let(:project_params) { { name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
let(:dev_env_or_com) { true }
let(:setup_for_company) { nil }
let(:combined_registration?) { true }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
......
......@@ -51,6 +51,8 @@ RSpec.describe Registrations::ProjectsController do
end
describe 'POST #create' do
let(:combined_registration?) { false }
it_behaves_like "Registrations::ProjectsController POST #create"
context 'force_company_trial_experiment' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Registrations::VerificationController do
let_it_be(:user) { create(:user) }
describe 'GET #new' do
subject { get :new }
context 'with an unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to(new_user_session_path) }
end
context 'with an authenticated user' do
let(:dev_env_or_com) { true }
before do
sign_in(user)
allow(::Gitlab).to receive(:dev_env_or_com?).and_return(dev_env_or_com)
end
context 'when on .com' do
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to render_template 'layouts/minimal' }
it { is_expected.to render_template(:new) }
it 'publishes the experiment' do
expect_next_instance_of(ApplicationExperiment) do |instance|
expect(instance).to receive(:publish_to_client)
end
subject
end
end
context 'when not on .com' do
let(:dev_env_or_com) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
end
end
import { GlLoadingIcon, GlModal, GlSprintf } from '@gitlab/ui';
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AccountVerificationModal from 'ee/billings/components/account_verification_modal.vue';
......@@ -6,7 +6,7 @@ describe('Account verification modal', () => {
let wrapper;
const createComponent = () => {
return shallowMount(AccountVerificationModal, {
wrapper = shallowMount(AccountVerificationModal, {
propsData: {
iframeUrl: 'https://gitlab.com',
allowedOrigin: 'https://gitlab.com',
......@@ -17,44 +17,37 @@ describe('Account verification modal', () => {
});
};
afterEach(() => {
wrapper.destroy();
});
describe('on destroying', () => {
it('removes message event listener', () => {
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
wrapper = createComponent();
const findModal = () => wrapper.find({ ref: 'modal' });
wrapper.destroy();
const zuoraSubmitSpy = jest.fn();
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'message',
wrapper.vm.handleFrameMessages,
true,
);
});
afterEach(() => {
wrapper.destroy();
});
describe('on creation', () => {
beforeEach(() => {
wrapper = createComponent();
createComponent();
});
afterEach(() => {
wrapper.destroy();
it('renders the title', () => {
expect(findModal().attributes('title')).toBe('Validate user account');
});
it('is in the loading state', () => {
expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true);
it('renders the description', () => {
expect(wrapper.find('p').text()).toContain('To use free pipeline minutes');
});
});
it('renders the title', () => {
expect(wrapper.findComponent(GlModal).attributes('title')).toBe('Validate user account');
describe('clicking the submit button', () => {
beforeEach(() => {
createComponent();
wrapper.vm.$refs.zuora = { submit: zuoraSubmitSpy };
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
});
it('renders the description', () => {
expect(wrapper.find('p').text()).toContain('To use free pipeline minutes');
it('calls the submit method of the Zuora component', () => {
expect(zuoraSubmitSpy).toHaveBeenCalled();
});
});
});
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Zuora from 'ee/billings/components/zuora.vue';
describe('Zuora', () => {
let wrapper;
const createComponent = (data = {}) => {
wrapper = shallowMount(Zuora, {
propsData: {
iframeUrl: 'https://gitlab.com',
allowedOrigin: 'https://gitlab.com',
initialHeight: 300,
},
data() {
return data;
},
});
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
let addEventListenerSpy;
let postMessageSpy;
let removeEventListenerSpy;
afterEach(() => {
wrapper.destroy();
});
describe('on creation', () => {
beforeEach(() => {
createComponent();
});
it('is in the loading state', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('when iframe loaded', () => {
beforeEach(() => {
addEventListenerSpy = jest.spyOn(window, 'addEventListener');
createComponent();
wrapper.vm.handleFrameLoaded();
});
it('is not in the loading state', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('adds an event listener', () => {
expect(addEventListenerSpy).toHaveBeenCalledWith(
'message',
wrapper.vm.handleFrameMessages,
true,
);
});
});
describe('on submit', () => {
beforeEach(() => {
createComponent({
error: 'an error occurred',
isLoading: false,
iframeHeight: 400,
});
wrapper.vm.$refs.zuora = { contentWindow: { postMessage: jest.fn() } };
postMessageSpy = jest.spyOn(wrapper.vm.$refs.zuora.contentWindow, 'postMessage');
wrapper.vm.submit();
});
it('hides the alert', () => {
expect(findAlert().exists()).toBe(false);
});
it('is in the loading state', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('resets the height to the initial height', () => {
expect(wrapper.vm.iframeHeight).toBe(300);
});
it('posts the submit message to the iframe', () => {
expect(postMessageSpy).toHaveBeenCalledWith('submit', 'https://gitlab.com');
});
});
describe('when showing an alert', () => {
beforeEach(() => {
createComponent({ error: 'an error occurred' });
});
it('shows the alert', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe('an error occurred');
});
describe('when dismissing the alert', () => {
beforeEach(() => {
findAlert().vm.$emit('dismiss');
});
it('hides the alert', () => {
expect(findAlert().exists()).toBe(false);
});
});
});
describe('handling iframe messages', () => {
beforeEach(() => {
createComponent();
});
describe('when success', () => {
beforeEach(() => {
wrapper.vm.handleFrameMessages({ origin: 'https://gitlab.com', data: { success: true } });
});
it('emits the success event', () => {
expect(wrapper.emitted('success')).toBeDefined();
});
});
describe('when not from an allowed origin', () => {
beforeEach(() => {
wrapper.vm.handleFrameMessages({ origin: 'https://test.com', data: { success: true } });
});
it('emits no event', () => {
expect(wrapper.emitted()).toEqual({});
});
});
describe('when failure and code less than 7', () => {
beforeEach(() => {
wrapper.vm.handleFrameMessages({
origin: 'https://gitlab.com',
data: { success: false, code: 6 },
});
});
it('emits no event', () => {
expect(wrapper.emitted()).toEqual({});
});
it('increases the iframe height', () => {
expect(wrapper.vm.iframeHeight).toBe(315);
});
});
describe('when failure and code greater than 6', () => {
beforeEach(() => {
removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
wrapper.vm.handleFrameMessages({
origin: 'https://gitlab.com',
data: { success: false, code: 7, msg: 'error' },
});
});
it('emits the failure event with the error message', () => {
expect(wrapper.emitted('failure')[0]).toEqual([{ msg: 'error' }]);
});
it('removes the message event listener', () => {
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'message',
wrapper.vm.handleFrameMessages,
true,
);
});
});
});
describe('on destroying', () => {
beforeEach(() => {
createComponent();
removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
wrapper.destroy();
});
it('removes the message event listener', () => {
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'message',
wrapper.vm.handleFrameMessages,
true,
);
});
});
});
import { GlButton, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Verification from 'ee/registrations/verification/components/verification.vue';
import {
IFRAME_MINIMUM_HEIGHT,
EVENT_LABEL,
MOUNTED_EVENT,
SKIPPED_EVENT,
VERIFIED_EVENT,
} from 'ee/registrations/verification/constants';
jest.mock('~/lib/utils/url_utility');
describe('Verification', () => {
let wrapper;
let trackingSpy;
let zuoraSubmitSpy;
const NEXT_STEP_URL = 'https://gitlab.com/next-step';
const IFRAME_URL = 'https://customers.gitlab.com/payment_forms/cc_registration_validation';
const ALLOWED_ORIGIN = 'https://customers.gitlab.com';
const createComponent = () => {
return shallowMount(Verification, {
provide: {
nextStepUrl: NEXT_STEP_URL,
},
stubs: {
GlButton,
GlPopover,
},
});
};
const findSubmitButton = () => wrapper.find({ ref: 'submitButton' });
const findZuora = () => wrapper.find({ ref: 'zuora' });
const findSkipLink = () => wrapper.find({ ref: 'skipLink' });
const findPopover = () => wrapper.find({ ref: 'popover' });
const findPopoverClose = () => wrapper.find({ ref: 'popoverClose' });
const findSkipConfirmationLink = () => wrapper.find({ ref: 'skipConfirmationLink' });
const expectRedirect = () => expect(redirectTo).toHaveBeenCalledWith(NEXT_STEP_URL);
const expectTrackingOfEvent = (event) => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, event, {
label: EVENT_LABEL,
});
};
beforeEach(() => {
window.gon = {
registration_validation_form_url: IFRAME_URL,
subscriptions_url: ALLOWED_ORIGIN,
};
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
unmockTracking();
window.gon = {};
});
describe('when the component is mounted', () => {
it('sends the mounted event', () => {
expectTrackingOfEvent(MOUNTED_EVENT);
});
it('renders the Zuora component with the right attributes', () => {
expect(findZuora().exists()).toBe(true);
expect(findZuora().attributes()).toMatchObject({
iframeurl: IFRAME_URL,
allowedorigin: ALLOWED_ORIGIN,
initialheight: IFRAME_MINIMUM_HEIGHT.toString(),
});
});
});
describe('when the submit button is clicked', () => {
beforeEach(() => {
zuoraSubmitSpy = jest.fn();
wrapper.vm.$refs.zuora = { submit: zuoraSubmitSpy };
findSubmitButton().trigger('click');
});
it('calls the submit method of the Zuora component', () => {
expect(zuoraSubmitSpy).toHaveBeenCalled();
});
});
describe('when the Zuora component emits a success event', () => {
beforeEach(() => {
findZuora().vm.$emit('success');
});
it('tracks the verified event', () => {
expectTrackingOfEvent(VERIFIED_EVENT);
});
it('redirects to the provided next step URL', () => {
expectRedirect();
});
});
describe('when the skip link is clicked', () => {
beforeEach(() => {
findSkipLink().trigger('click');
});
it('shows the popover', () => {
expect(findPopover().exists()).toBe(true);
});
describe('when the skip confirmation link in the popover is clicked', () => {
beforeEach(() => {
findSkipConfirmationLink().vm.$emit('click');
});
it('tracks the skipped event', () => {
expectTrackingOfEvent(SKIPPED_EVENT);
});
it('redirects to the provided next step URL', () => {
expectRedirect();
});
});
describe('when closing the popover', () => {
beforeEach(() => {
findPopoverClose().trigger('click');
});
it('hides the popover', () => {
expect(findPopover().exists()).toBe(false);
});
describe('when clicking the skip link again', () => {
beforeEach(() => {
findSkipLink().trigger('click');
});
it('tracks the skipped event', () => {
expectTrackingOfEvent(SKIPPED_EVENT);
});
it('redirects to the provided next step URL', () => {
expectRedirect();
});
});
});
});
});
......@@ -35,4 +35,89 @@ RSpec.describe EE::RegistrationsHelper do
end
end
end
describe '#registration_verification_enabled?' do
let_it_be(:current_user) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(current_user)
end
subject(:action) { helper.registration_verification_enabled? }
context 'when experiment is candidate' do
before do
stub_experiments(registration_verification: :candidate)
end
it { is_expected.to eq(true) }
end
context 'when experiment is control' do
before do
stub_experiments(registration_verification: :control)
end
it { is_expected.to be_falsey }
end
it_behaves_like 'tracks assignment and records the subject', :registration_verification, :user do
subject { current_user }
end
end
describe '#registration_verification_data' do
before do
allow(helper).to receive(:params).and_return(ActionController::Parameters.new(params))
allow(helper).to receive(:current_user).and_return(build(:user, setup_for_company: setup_for_company))
end
let(:setup_for_company) { false }
context 'with `learn_gitlab_project_id` parameter present' do
let(:params) { { learn_gitlab_project_id: 1 } }
it 'return expected data' do
expect(helper.registration_verification_data)
.to eq(next_step_url: helper.trial_getting_started_users_sign_up_welcome_path(params))
end
end
context 'with `project_id` parameter present' do
let(:params) { { project_id: 1 } }
it 'return expected data' do
expect(helper.registration_verification_data)
.to eq(next_step_url: helper.continuous_onboarding_getting_started_users_sign_up_welcome_path(params))
end
end
context 'with `combined` parameter present' do
let(:params) { { combined: true } }
context 'when user is setting up for a company' do
let(:setup_for_company) { true }
it 'return expected data' do
expect(helper.registration_verification_data)
.to eq(next_step_url: helper.new_trial_path)
end
end
context 'when user is not setting up for a company' do
it 'return expected data' do
expect(helper.registration_verification_data)
.to eq(next_step_url: helper.root_path)
end
end
end
context 'with no relevant parameters present' do
let(:params) { { xxx: 1 } }
it 'return expected data' do
expect(helper.registration_verification_data).to eq(next_step_url: helper.root_path)
end
end
end
end
......@@ -34,6 +34,7 @@ RSpec.describe EE::Gitlab::GonHelper do
it 'includes CustomersDot variables' do
expect(gon).to receive(:subscriptions_url=).with(Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL)
expect(gon).to receive(:payment_form_url=).with(Gitlab::SubscriptionPortal::PAYMENT_FORM_URL)
expect(gon).to receive(:registration_validation_form_url=).with(Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL)
helper.add_gon_variables
end
......
......@@ -40,6 +40,22 @@ RSpec.shared_examples "Registrations::ProjectsController POST #create" do
expect(controller.stored_location_for(:user)).to eq(stored_location_for)
end
context 'when the `registration_verification` experiment is enabled' do
before do
stub_experiments(registration_verification: :candidate)
allow_next_instance_of(::Projects::CreateService) do |service|
allow(service).to receive(:execute).and_return(first_project)
end
end
it 'is expected to redirect to the verification page' do
params = { project_id: first_project.id }
params[:combined] = true if combined_registration?
expect(subject).to redirect_to(new_users_sign_up_verification_path(params))
end
end
context 'learn gitlab project' do
using RSpec::Parameterized::TableSyntax
......@@ -86,6 +102,22 @@ RSpec.shared_examples "Registrations::ProjectsController POST #create" do
end
expect(subject).to redirect_to(trial_getting_started_users_sign_up_welcome_path(learn_gitlab_project_id: project.id))
end
context 'when the `registration_verification` experiment is enabled' do
before do
stub_experiments(registration_verification: :candidate)
allow_next_instance_of(::Projects::GitlabProjectsImportService) do |service|
allow(service).to receive(:execute).and_return(project)
end
end
it 'is expected to redirect to the verification page' do
params = { learn_gitlab_project_id: project.id }
params[:combined] = true if combined_registration?
expect(subject).to redirect_to(new_users_sign_up_verification_path(params))
end
end
end
context 'when the project cannot be saved' do
......
......@@ -18,6 +18,10 @@ module Gitlab
"#{self.subscriptions_url}/payment_forms/cc_validation"
end
def self.registration_validation_form_url
"#{self.subscriptions_url}/payment_forms/cc_registration_validation"
end
def self.subscriptions_comparison_url
'https://about.gitlab.com/pricing/gitlab-com/feature-comparison'
end
......@@ -84,3 +88,4 @@ Gitlab::SubscriptionPortal.prepend_mod
Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL = Gitlab::SubscriptionPortal.subscriptions_url.freeze
Gitlab::SubscriptionPortal::PAYMENT_FORM_URL = Gitlab::SubscriptionPortal.payment_form_url.freeze
Gitlab::SubscriptionPortal::RENEWAL_SERVICE_EMAIL = Gitlab::SubscriptionPortal.renewal_service_email.freeze
Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL = Gitlab::SubscriptionPortal.registration_validation_form_url.freeze
......@@ -29097,6 +29097,36 @@ msgstr ""
msgid "RegistrationFeatures|Want to use this feature for free?"
msgstr ""
msgid "RegistrationVerification|Are you sure you want to skip this step?"
msgstr ""
msgid "RegistrationVerification|Enable free CI/CD minutes"
msgstr ""
msgid "RegistrationVerification|GitLab will not charge your card, it will only be used for validation."
msgstr ""
msgid "RegistrationVerification|Pipelines using shared GitLab runners will fail until you validate your account."
msgstr ""
msgid "RegistrationVerification|Skip this for now"
msgstr ""
msgid "RegistrationVerification|To keep GitLab spam and abuse free we ask that you verify your identity with a valid payment method, such as a debit or credit card. Until then, you can't use free CI/CD minutes to build your application."
msgstr ""
msgid "RegistrationVerification|Validate account"
msgstr ""
msgid "RegistrationVerification|Verify your identity"
msgstr ""
msgid "RegistrationVerification|Yes, I'd like to skip"
msgstr ""
msgid "RegistrationVerification|You can alway verify your account at a later time."
msgstr ""
msgid "Registration|Checkout"
msgstr ""
......
......@@ -56,6 +56,7 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
where(:method_name, :result) do
:default_subscriptions_url | 'https://customers.staging.gitlab.com'
:payment_form_url | 'https://customers.staging.gitlab.com/payment_forms/cc_validation'
:registration_validation_form_url | 'https://customers.staging.gitlab.com/payment_forms/cc_registration_validation'
:subscriptions_graphql_url | 'https://customers.staging.gitlab.com/graphql'
:subscriptions_more_minutes_url | 'https://customers.staging.gitlab.com/buy_pipeline_minutes'
:subscriptions_more_storage_url | 'https://customers.staging.gitlab.com/buy_storage'
......
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