Commit f98dc453 authored by Eugie Limpin's avatar Eugie Limpin Committed by Luke Duncalfe

Experiment: Require verification before create group

Issue: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/533/
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77569

This implements an **experiment** where we require users to 
verify their identity (by providing credit card payment details) 
before allowing them to create new groups.
parent 78333253
......@@ -15,7 +15,7 @@ initFilePickers();
new Group(); // eslint-disable-line no-new
function initNewGroupCreation(el) {
const { hasErrors } = el.dataset;
const { hasErrors, verificationRequired, verificationFormUrl, subscriptionsUrl } = el.dataset;
const props = {
hasErrors: parseBoolean(hasErrors),
......@@ -23,6 +23,11 @@ function initNewGroupCreation(el) {
return new Vue({
el,
provide: {
verificationRequired: parseBoolean(verificationRequired),
verificationFormUrl,
subscriptionsUrl,
},
render(h) {
return h(NewGroupCreationApp, { props });
},
......
......@@ -10,10 +10,17 @@ export default {
GlIcon,
WelcomePage,
LegacyContainer,
CreditCardVerification: () =>
import('ee_component/pages/groups/new/components/credit_card_verification.vue'),
},
directives: {
SafeHtml,
},
inject: {
verificationRequired: {
default: false,
},
},
props: {
title: {
type: String,
......@@ -41,6 +48,7 @@ export default {
data() {
return {
activePanelName: null,
verificationCompleted: false,
};
},
......@@ -67,6 +75,10 @@ export default {
{ text: this.activePanel.title, href: `#${this.activePanel.name}` },
];
},
shouldVerify() {
return this.verificationRequired && !this.verificationCompleted;
},
},
created() {
......@@ -93,12 +105,16 @@ export default {
localStorage.setItem(this.persistenceKey, this.activePanelName);
}
},
onVerified() {
this.verificationCompleted = true;
},
},
};
</script>
<template>
<welcome-page v-if="!activePanelName" :panels="panels" :title="title">
<credit-card-verification v-if="shouldVerify" @verified="onVerified" />
<welcome-page v-else-if="!activePanelName" :panels="panels" :title="title">
<template #footer>
<slot name="welcome-footer"> </slot>
</template>
......
......@@ -134,6 +134,16 @@ module GroupsHelper
@group_projects_sort || @sort || params[:sort] || sort_value_recently_created
end
def verification_for_group_creation_data
# overridden in EE
{}
end
def require_verification_for_group_creation_enabled?
# overridden in EE
false
end
private
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
......
......@@ -6,7 +6,7 @@
.group-edit-container.gl-mt-5
.js-new-group-creation{ data: { has_errors: @group.errors.any?.to_s } }
.js-new-group-creation{ data: { has_errors: @group.errors.any?.to_s }.merge(verification_for_group_creation_data) }
.row{ 'v-cloak': true }
#create-group-pane.tab-pane
......
---
name: require_verification_for_group_creation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77569
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349857
milestone: '14.7'
type: experiment
group: group::activation
default_enabled: false
......@@ -45,6 +45,11 @@ export default {
return `${this.iframeUrl}?${objectToQuery(query)}`;
},
},
watch: {
isLoading(value) {
this.$emit('loading', value);
},
},
destroyed() {
window.removeEventListener('message', this.handleFrameMessages, true);
},
......
<script>
import { GlBreadcrumb, GlButton, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import newGroupIllustration from '@gitlab/svgs/dist/illustrations/group-new.svg';
import CreateGroupDescriptionDetails from '~/pages/groups/new/components/create_group_description_details.vue';
import Zuora from 'ee/billings/components/zuora.vue';
import { s__ } from '~/locale';
const I18N_SIDE_PANE_TITLE = s__('GroupsNew|Create group');
const I18N_FORM_TITLE = s__('IdentityVerification|Verify your identity');
const I18N_FORM_EXPLANATION = s__(
'IdentityVerification|Before you create your group, we need you to verify your identity with a valid payment method.',
);
const I18N_FORM_SUBMIT = s__('IdentityVerification|Verify your identity');
export default {
components: {
GlBreadcrumb,
GlButton,
CreateGroupDescriptionDetails,
Zuora,
},
directives: {
SafeHtml,
},
inject: ['verificationFormUrl', 'subscriptionsUrl'],
data() {
return {
iframeUrl: this.verificationFormUrl,
allowedOrigin: this.subscriptionsUrl,
isLoading: true,
};
},
methods: {
updateIsLoading(isLoading) {
this.isLoading = isLoading;
},
verified() {
this.$emit('verified');
},
submit() {
this.$refs.zuora.submit();
},
},
illustration: newGroupIllustration,
I18N_SIDE_PANE_TITLE,
I18N_FORM_TITLE,
I18N_FORM_EXPLANATION,
I18N_FORM_SUBMIT,
};
</script>
<template>
<div class="row">
<div class="col-lg-3">
<div v-safe-html="$options.illustration" class="gl-text-white"></div>
<h4>{{ $options.I18N_SIDE_PANE_TITLE }}</h4>
<create-group-description-details />
</div>
<div class="col-lg-9">
<gl-breadcrumb :items="[]" />
<label class="gl-mt-3">{{ $options.I18N_FORM_TITLE }}</label>
<p>{{ $options.I18N_FORM_EXPLANATION }}</p>
<zuora
ref="zuora"
:initial-height="328"
:iframe-url="iframeUrl"
:allowed-origin="allowedOrigin"
@success="verified"
@loading="updateIsLoading"
/>
<gl-button variant="confirm" type="submit" :disabled="isLoading" @click="submit">{{
$options.I18N_FORM_SUBMIT
}}</gl-button>
</div>
</div>
</template>
......@@ -59,5 +59,26 @@ module EE
def show_product_purchase_success_alert?
!params[:purchased_product].blank?
end
override :require_verification_for_group_creation_enabled?
def require_verification_for_group_creation_enabled?
# Experiment should only run when creating top-level groups
return false if params[:parent_id]
experiment(:require_verification_for_group_creation, user: current_user) do |e|
e.candidate { true }
e.control { false }
e.run
end
end
override :verification_for_group_creation_data
def verification_for_group_creation_data
{
verification_required: require_verification_for_group_creation_enabled?.to_s,
verification_form_url: ::Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL,
subscriptions_url: ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL
}
end
end
end
......@@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe 'New Group page' do
let_it_be(:user) { create(:user) }
describe 'toggling the invite members section', :js do
before do
sign_in(create(:user))
sign_in(user)
visit new_group_path
click_link 'Create group'
end
......@@ -20,4 +22,57 @@ RSpec.describe 'New Group page' do
end
end
end
describe 'identity verification experiment', :js do
let(:variant) { :control }
let(:query_params) { {} }
subject(:visit_new_group_page) do
sign_in(user)
visit new_group_path(query_params)
end
before do
stub_experiments(require_verification_for_group_creation: variant)
end
context 'when creating a top-level group' do
before do
visit_new_group_page
end
it 'does not show verification form' do
expect(page).not_to have_content('Verify your identity')
expect(page).not_to have_content('Before you create your group')
expect(page).to have_content('Create new group')
end
context 'when in candidate path' do
let(:variant) { :candidate }
it 'shows verification form' do
expect(page).to have_content('Verify your identity')
expect(page).to have_content('Before you create your group')
expect(page).not_to have_content('Create new group')
end
end
end
context 'when creating a sub-group' do
let(:parent_group) { create(:group) }
let(:query_params) { { parent_id: parent_group.id } }
let(:variant) { :candidate }
before do
parent_group.add_owner(user)
visit_new_group_page
end
it 'does not show verification form' do
expect(page).not_to have_content('Verify your identity')
expect(page).not_to have_content('Before you create your group')
expect(page).to have_content('Create new group')
end
end
end
end
......@@ -142,8 +142,10 @@ describe('Zuora', () => {
});
});
it('emits no event', () => {
expect(wrapper.emitted()).toEqual({});
it('emits only loading event with value `false`', () => {
expect(Object.keys(wrapper.emitted())).toHaveLength(1);
expect(wrapper.emitted('loading')).toHaveLength(1);
expect(wrapper.emitted('loading')[0]).toEqual([false]);
});
it('increases the iframe height', () => {
......@@ -164,6 +166,11 @@ describe('Zuora', () => {
expect(wrapper.emitted('failure')[0]).toEqual([{ msg: 'error' }]);
});
it('emits loading event with value `false`', () => {
expect(wrapper.emitted('loading')).toHaveLength(1);
expect(wrapper.emitted('loading')[0]).toEqual([false]);
});
it('removes the message event listener', () => {
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'message',
......
import { GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CreditCardVerification from 'ee/pages/groups/new/components/credit_card_verification.vue';
describe('Verification page', () => {
let wrapper;
const DEFAULT_PROVIDES = {
verificationFormUrl: 'https://gitlab.com',
subscriptionsUrl: 'https://gitlab.com',
};
const createComponent = (opts) => {
wrapper = mountExtended(CreditCardVerification, {
provide: DEFAULT_PROVIDES,
...opts,
});
};
afterEach(() => {
wrapper.destroy();
});
describe('on creation', () => {
beforeEach(() => {
createComponent();
});
it('renders the title', () => {
expect(wrapper.findByText('Verify your identity').exists()).toBe(true);
});
it('renders the explanation', () => {
expect(
wrapper
.findByText(
'Before you create your group, we need you to verify your identity with a valid payment method.',
)
.exists(),
).toBe(true);
});
});
describe('successful verification', () => {
let mockPostMessage;
beforeEach(() => {
const dispatchWindowMessageEvent = () => {
window.dispatchEvent(
new MessageEvent('message', {
origin: DEFAULT_PROVIDES.subscriptionsUrl,
data: { success: true },
}),
);
};
createComponent({
attachTo: document.body,
});
// mock load event so success event listeners are registered
wrapper.find('iframe').trigger('load');
// mock success event arrival when postMessage is called on the Zuora iframe
mockPostMessage = jest
.spyOn(wrapper.find('iframe').element.contentWindow, 'postMessage')
.mockImplementation(dispatchWindowMessageEvent);
wrapper.find(GlButton).vm.$emit('click');
});
it('triggers postMessage on the Zuora iframe', () => {
expect(mockPostMessage).toHaveBeenCalledWith('submit', DEFAULT_PROVIDES.subscriptionsUrl);
});
it('emits verified event', () => {
expect(wrapper.emitted('verified')).toHaveLength(1);
});
});
});
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
import VerificationPage from 'ee_component/pages/groups/new/components/credit_card_verification.vue';
import Zuora from 'ee/billings/components/zuora.vue';
describe('Experimental new project creation app', () => {
let wrapper;
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findVerificationPage = () => wrapper.findComponent(VerificationPage);
const findZuora = () => wrapper.findComponent(Zuora);
const DEFAULT_PROPS = {
title: 'Create something',
initialBreadcrumb: 'Something',
panels: [
{ name: 'panel1', selector: '#some-selector1' },
{ name: 'panel2', selector: '#some-selector2' },
],
persistenceKey: 'DEMO-PERSISTENCE-KEY',
};
const DEFAULT_PROVIDES = {
verificationFormUrl: 'https://gitlab.com',
subscriptionsUrl: 'https://gitlab.com',
};
const createComponent = ({ provide = {} } = {}) => {
wrapper = mount(NewNamespacePage, {
propsData: DEFAULT_PROPS,
provide: { ...DEFAULT_PROVIDES, ...provide },
});
};
afterEach(() => {
wrapper.destroy();
});
it('does not show verification page', () => {
createComponent();
expect(findVerificationPage().exists()).toBe(false);
});
describe('when verificationRequired true', () => {
beforeEach(() => {
createComponent({ provide: { verificationRequired: true } });
});
it('does not show welcome page', () => {
expect(findWelcomePage().exists()).toBe(false);
});
it('shows verification page', () => {
expect(findVerificationPage().exists()).toBe(true);
});
describe('when verificationCompleted becomes true', () => {
beforeEach(() => {
findVerificationPage().vm.$refs.zuora = {
submit: jest.fn(() => {
findZuora().vm.$emit('success');
}),
};
wrapper.findComponent(GlButton).vm.$emit('click');
});
it('shows welcome page', () => {
expect(findWelcomePage().exists()).toBe(true);
});
it('does not show verification page', () => {
expect(findVerificationPage().exists()).toBe(false);
});
});
});
});
......@@ -222,4 +222,32 @@ RSpec.describe GroupsHelper do
it { expect(helper.show_product_purchase_success_alert?).to be false }
end
end
describe '#require_verification_for_group_creation_enabled?' do
let(:variant) { :control }
subject { helper.require_verification_for_group_creation_enabled? }
before do
stub_experiments(require_verification_for_group_creation: variant)
end
context 'when in candidate path' do
let(:variant) { :candidate }
it { is_expected.to eq(true) }
context 'when creating a sub-group' do
before do
allow(controller).to receive(:params) { { parent_id: 1 } }
end
it { is_expected.to eq(false) }
end
end
context 'when in control path' do
it { is_expected.to eq(false) }
end
end
end
......@@ -17847,6 +17847,12 @@ msgstr ""
msgid "Identities"
msgstr ""
msgid "IdentityVerification|Before you create your group, we need you to verify your identity with a valid payment method."
msgstr ""
msgid "IdentityVerification|Verify your identity"
msgstr ""
msgid "If any indexed field exceeds this limit, it is truncated to this number of characters. The rest of the content is neither indexed nor searchable. This does not apply to repository and wiki indexing. For unlimited characters, set this to 0."
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