Commit 0c7f4366 authored by Dmitry Gruzd's avatar Dmitry Gruzd

Merge branch '273626-add-ctas-and-click-tracking-to-the-paid-feature-callout-popover' into 'master'

Add CTA buttons & track their usage

See merge request gitlab-org/gitlab!58832
parents 34c31163 35d7d3fa
<script>
import { GlPopover } from '@gitlab/ui';
import { GlButton, GlPopover } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { debounce } from 'lodash';
import { __, n__, s__, sprintf } from '~/locale';
......@@ -8,7 +8,13 @@ import Tracking from '~/tracking';
const RESIZE_EVENT_DEBOUNCE_MS = 150;
export default {
tracking: {
action: 'click_button',
labels: { upgrade: 'upgrade_to_ultimate', compare: 'compare_all_plans' },
property: 'experiment:highlight_paid_features_during_active_trial',
},
components: {
GlButton,
GlPopover,
},
mixins: [Tracking.mixin()],
......@@ -26,6 +32,14 @@ export default {
type: String,
required: true,
},
hrefComparePlans: {
type: String,
required: true,
},
hrefUpgradeToPaid: {
type: String,
required: true,
},
planNameForTrial: {
type: String,
required: true,
......@@ -54,6 +68,9 @@ export default {
disabled: false,
};
},
i18n: {
compareAllButtonTitle: s__('BillingPlans|Compare all plans'),
},
computed: {
popoverTitle() {
const i18nPopoverTitle = n__(
......@@ -77,6 +94,13 @@ export default {
planNameForUpgrade: this.planNameForUpgrade,
});
},
upgradeButtonTitle() {
const i18nUpgradeButtonTitle = s__('BillingPlans|Upgrade to GitLab %{planNameForUpgrade}');
return sprintf(i18nUpgradeButtonTitle, {
planNameForUpgrade: this.planNameForUpgrade,
});
},
},
created() {
this.debouncedResize = debounce(() => this.onResize(), RESIZE_EVENT_DEBOUNCE_MS);
......@@ -95,7 +119,7 @@ export default {
onShown() {
this.track('popover_shown', {
label: `feature_highlight_popover:${this.featureName}`,
property: 'experiment:highlight_paid_features_during_active_trial',
property: this.$options.tracking.property,
});
},
updateDisabledState() {
......@@ -128,5 +152,38 @@ export default {
</div>
{{ popoverContent }}
<div class="gl-mt-5">
<gl-button
:href="hrefUpgradeToPaid"
target="_blank"
category="primary"
variant="confirm"
size="small"
class="gl-mb-0"
block
data-testid="upgradeBtn"
:data-track-action="$options.tracking.action"
:data-track-label="$options.tracking.labels.upgrade"
:data-track-property="$options.tracking.property"
>
<span class="gl-font-sm">{{ upgradeButtonTitle }}</span>
</gl-button>
<gl-button
:href="hrefComparePlans"
target="_blank"
category="secondary"
variant="confirm"
size="small"
class="gl-mb-0"
block
data-testid="compareBtn"
:data-track-action="$options.tracking.action"
:data-track-label="$options.tracking.labels.compare"
:data-track-property="$options.tracking.property"
>
<span class="gl-font-sm">{{ $options.i18n.compareAllButtonTitle }}</span>
</gl-button>
</div>
</gl-popover>
</template>
......@@ -25,6 +25,8 @@ export const initPaidFeatureCalloutPopover = () => {
containerId,
daysRemaining,
featureName,
hrefComparePlans,
hrefUpgradeToPaid,
planNameForTrial,
planNameForUpgrade,
promoImageAltText,
......@@ -40,6 +42,8 @@ export const initPaidFeatureCalloutPopover = () => {
containerId,
daysRemaining: Number(daysRemaining),
featureName,
hrefComparePlans,
hrefUpgradeToPaid,
planNameForTrial,
planNameForUpgrade,
promoImageAltText,
......
......@@ -28,6 +28,8 @@ module PaidFeatureCalloutHelper
base_attrs.merge({
container_id: container_id,
days_remaining: group.trial_days_remaining,
href_compare_plans: group_billings_path(group),
href_upgrade_to_paid: premium_subscription_path_for_group(group),
plan_name_for_trial: group.gitlab_subscription&.plan_title,
plan_name_for_upgrade: 'Premium',
target_id: container_id
......@@ -43,4 +45,16 @@ module PaidFeatureCalloutHelper
def base_paid_feature_data_attrs(feature_name)
{ feature_name: feature_name }
end
def premium_subscription_path_for_group(group)
# NOTE: We are okay hard-coding the production value for the Premium 1-year
# SaaS plan ID while this is all part of an active experiment. If & when the
# experiment is deemed a success, part of the clean-up effort will be to
# pull the value directly from the CustomersDot API. Value taken from
# https://gitlab.com/gitlab-org/customers-gitlab-com/blob/7177f13c478ef623b779d6635c4a58ee650b7884/config/application.yml#L186
# Cleanup issue: https://gitlab.com/gitlab-org/gitlab/-/issues/330987
zuora_premium_plan_id = '2c92a00d76f0d5060176f2fb0a5029ff'
new_subscriptions_path(namespace_id: group.id, plan_id: zuora_premium_plan_id)
end
end
......@@ -47,7 +47,13 @@ module TrialStatusWidgetHelper
end
def ultimate_subscription_path_for_group(group)
# Hard-coding the plan_id to the Ultimate plan on production & staging
new_subscriptions_path(namespace_id: group.id, plan_id: '2c92a0fc5a83f01d015aa6db83c45aac')
# NOTE: We are okay hard-coding the production value for the Ulitmate 1-year
# SaaS plan ID while this is all part of an active experiment. If & when the
# experiment is deemed a success, part of the clean-up effort will be to
# pull the value directly from the CustomersDot API. Value taken from
# https://gitlab.com/gitlab-org/customers-gitlab-com/blob/7177f13c478ef623b779d6635c4a58ee650b7884/config/application.yml#L207
zuora_ultimate_plan_id = '2c92a0ff76f0d5250176f2f8c86f305a'
new_subscriptions_path(namespace_id: group.id, plan_id: zuora_ultimate_plan_id)
end
end
......@@ -10,72 +10,92 @@ describe('PaidFeatureCalloutPopover', () => {
let trackingSpy;
let wrapper;
const trackingExperimentKey = 'experiment:highlight_paid_features_during_active_trial';
const findGlPopover = () => wrapper.findComponent(GlPopover);
const defaultProps = {
daysRemaining: 12,
featureName: 'some feature',
planNameForTrial: 'Ultimate',
planNameForUpgrade: 'Premium',
hrefComparePlans: '/group/test-group/-/billings',
hrefUpgradeToPaid: '/-/subscriptions/new?namespace_id=123&plan_id=abc456',
planNameForTrial: 'Awesomesauce',
planNameForUpgrade: 'Amazing',
targetId: 'some-feature-callout-target',
};
const createComponent = (props = defaultProps) => {
const createComponent = (extraProps = {}) => {
return extendedWrapper(
shallowMount(PaidFeatureCalloutPopover, {
propsData: props,
propsData: {
...defaultProps,
...extraProps,
},
}),
);
};
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('with some default props', () => {
beforeEach(() => {
wrapper = createComponent();
describe('GlPopover attributes', () => {
const sharedAttrs = {
boundary: 'viewport',
placement: 'top',
target: 'some-feature-callout-target',
};
describe('with some default props', () => {
it('sets attributes on the GlPopover component', () => {
const attributes = findGlPopover().attributes();
expect(attributes).toMatchObject(sharedAttrs);
expect(attributes.containerId).toBeUndefined();
});
});
it('sets attributes on the GlPopover component', () => {
const attributes = findGlPopover().attributes();
describe('with additional, optional props', () => {
beforeEach(() => {
wrapper = createComponent({ containerId: 'some-container-id' });
});
expect(attributes).toMatchObject({
boundary: 'viewport',
placement: 'top',
target: 'some-feature-callout-target',
it('sets more attributes on the GlPopover component', () => {
expect(findGlPopover().attributes()).toMatchObject({
...sharedAttrs,
container: 'some-container-id',
});
});
expect(attributes.containerId).toBeUndefined();
});
});
describe('with additional, optional props', () => {
beforeEach(() => {
wrapper = createComponent({
...defaultProps,
containerId: 'some-container-id',
});
describe('popoverTitle', () => {
it('renders the title text', () => {
expect(wrapper.vm.popoverTitle).toEqual('12 days remaining to enjoy some feature');
});
});
it('sets more attributes on the GlPopover component', () => {
expect(findGlPopover().attributes()).toMatchObject({
boundary: 'viewport',
container: 'some-container-id',
placement: 'top',
target: 'some-feature-callout-target',
});
describe('popoverContent', () => {
it('renders the content text', () => {
expect(wrapper.vm.popoverContent).toEqual(
'Enjoying your GitLab Awesomesauce trial? To continue using some feature after your trial ends, upgrade to ' +
'GitLab Amazing.',
);
});
});
describe('promo image', () => {
const promoImagePathForTest = 'path/to/some/image.svg';
const findPromoImage = () => wrapper.findByTestId('promo-img');
describe('with the optional promoImagePath prop', () => {
beforeEach(() => {
wrapper = createComponent({
...defaultProps,
promoImagePath: 'path/to/some/image.svg',
});
wrapper = createComponent({ promoImagePath: promoImagePathForTest });
});
it('renders the promo image', () => {
......@@ -85,8 +105,7 @@ describe('PaidFeatureCalloutPopover', () => {
describe('with the optional promoImageAltText prop', () => {
beforeEach(() => {
wrapper = createComponent({
...defaultProps,
promoImagePath: 'path/to/some/image.svg',
promoImagePath: promoImagePathForTest,
promoImageAltText: 'My fancy alt text',
});
});
......@@ -97,13 +116,6 @@ describe('PaidFeatureCalloutPopover', () => {
});
describe('without the optional promoImageAltText prop', () => {
beforeEach(() => {
wrapper = createComponent({
...defaultProps,
promoImagePath: 'path/to/some/image.svg',
});
});
it('renders the promo image with default alt text', () => {
expect(findPromoImage().attributes('alt')).toBe('SVG illustration');
});
......@@ -111,16 +123,50 @@ describe('PaidFeatureCalloutPopover', () => {
});
describe('without the optional promoImagePath prop', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('does not render a promo image', () => {
expect(findPromoImage().exists()).toBe(false);
});
});
});
describe('call-to-action buttons', () => {
const sharedAttrs = {
target: '_blank',
variant: 'confirm',
size: 'small',
block: '',
'data-track-action': 'click_button',
'data-track-property': trackingExperimentKey,
};
const findUpgradeBtn = () => wrapper.findByTestId('upgradeBtn');
const findCompareBtn = () => wrapper.findByTestId('compareBtn');
it('correctly renders an Upgrade button', () => {
const upgradeBtn = findUpgradeBtn();
expect(upgradeBtn.text()).toEqual('Upgrade to GitLab Amazing');
expect(upgradeBtn.attributes()).toMatchObject({
...sharedAttrs,
href: '/-/subscriptions/new?namespace_id=123&plan_id=abc456',
category: 'primary',
'data-track-label': 'upgrade_to_ultimate',
});
});
it('correctly renders a Compare button', () => {
const compareBtn = findCompareBtn();
expect(compareBtn.text()).toEqual('Compare all plans');
expect(compareBtn.attributes()).toMatchObject({
...sharedAttrs,
href: '/group/test-group/-/billings',
category: 'secondary',
'data-track-label': 'compare_all_plans',
});
});
});
describe('onShown', () => {
beforeEach(() => {
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
......@@ -131,16 +177,12 @@ describe('PaidFeatureCalloutPopover', () => {
it('tracks that the popover has been shown', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'popover_shown', {
label: 'feature_highlight_popover:some feature',
property: 'experiment:highlight_paid_features_during_active_trial',
property: trackingExperimentKey,
});
});
});
describe('onResize', () => {
beforeEach(() => {
wrapper = createComponent();
});
it.each`
bp | disabled
${'xs'} | ${'true'}
......
......@@ -71,15 +71,17 @@ RSpec.describe PaidFeatureCalloutHelper do
describe '#paid_feature_popover_data_attrs' do
let(:subscription) { instance_double(GitlabSubscription, plan_title: 'Ultimate') }
let(:group) { instance_double(Group, trial_days_remaining: 12, gitlab_subscription: subscription) }
let(:group) { instance_double(Group, id: 123, to_param: 'test-group', trial_days_remaining: 12, gitlab_subscription: subscription) }
subject { helper.paid_feature_popover_data_attrs(group: group, feature_name: 'first feature') }
it 'returns the set of data attributes needed to bootstrap the PaidFeatureCalloutPopover component' do
expected_attrs = {
container_id: 'first-feature-callout',
feature_name: 'first feature',
days_remaining: 12,
feature_name: 'first feature',
href_compare_plans: '/groups/test-group/-/billings',
href_upgrade_to_paid: '/-/subscriptions/new?namespace_id=123&plan_id=2c92a00d76f0d5060176f2fb0a5029ff',
plan_name_for_trial: 'Ultimate',
plan_name_for_upgrade: 'Premium',
target_id: 'first-feature-callout'
......
......@@ -37,7 +37,7 @@ RSpec.describe TrialStatusWidgetHelper do
expect(data_attrs).to match(
shared_expected_attrs.merge(
group_name: 'Pants Group',
purchase_href: '/-/subscriptions/new?namespace_id=123&plan_id=2c92a0fc5a83f01d015aa6db83c45aac',
purchase_href: '/-/subscriptions/new?namespace_id=123&plan_id=2c92a0ff76f0d5250176f2f8c86f305a',
target_id: shared_expected_attrs[:container_id],
trial_end_date: Date.parse('2021-01-30')
)
......
......@@ -5027,6 +5027,9 @@ msgstr ""
msgid "BillingPlans|@%{user_name} you are currently using the %{plan_name}."
msgstr ""
msgid "BillingPlans|Compare all plans"
msgstr ""
msgid "BillingPlans|Congratulations, your free trial is activated."
msgstr ""
......@@ -5060,6 +5063,9 @@ msgstr ""
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr ""
msgid "BillingPlans|Upgrade to GitLab %{planNameForUpgrade}"
msgstr ""
msgid "BillingPlans|While GitLab is ending availability of the Bronze plan, you can still renew your Bronze subscription one additional time before %{eoa_bronze_plan_end_date}. We are also offering a limited time free upgrade to our Premium Plan (up to 25 users)! Learn more about the changes and offers in our %{announcement_link}."
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