Commit 111ae4c1 authored by Savas Vedova's avatar Savas Vedova

Merge branch '225834-add-specific-alert-when-newly-added-license-is-future-dated' into 'master'

Feat(Admin Subscriptions): Add future dated alert

See merge request gitlab-org/gitlab!69874
parents be4538b5 bdeb6ffc
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
import { isInFuture } from '~/lib/utils/datetime/date_calculation_utility';
import { sprintf } from '~/locale';
import {
activateSubscription,
noActiveSubscription,
subscriptionActivationNotificationText,
subscriptionActivationFutureDatedNotificationTitle,
subscriptionActivationFutureDatedNotificationMessage,
subscriptionHistoryQueries,
subscriptionMainTitle,
subscriptionQueries,
exportLicenseUsageBtnText,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from '../constants';
import SubscriptionActivationCard from './subscription_activation_card.vue';
import SubscriptionBreakdown from './subscription_breakdown.vue';
......@@ -28,7 +33,6 @@ export default {
activateSubscription,
exportLicenseUsageBtnText,
noActiveSubscription,
subscriptionActivationNotificationText,
subscriptionMainTitle,
},
props: {
......@@ -48,9 +52,6 @@ export default {
update({ currentLicense }) {
return currentLicense || {};
},
result({ data }) {
this.hasNewLicense = data?.currentLicense && !this.hasActiveLicense;
},
},
subscriptionHistory: {
query: subscriptionHistoryQueries.query,
......@@ -62,10 +63,11 @@ export default {
data() {
return {
currentSubscription: {},
hasDismissedNotification: false,
hasNewLicense: false,
activationNotification: null,
activationListeners: {
[SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT]: this.displayActivationNotification,
},
subscriptionHistory: [],
notification: null,
};
},
computed: {
......@@ -75,13 +77,22 @@ export default {
canShowSubscriptionDetails() {
return this.hasActiveLicense || this.hasValidSubscriptionData;
},
shouldShowActivationNotification() {
return !this.hasDismissedNotification && this.hasNewLicense && this.hasValidSubscriptionData;
},
},
methods: {
dismissSuccessAlert() {
this.hasDismissedNotification = true;
displayActivationNotification(license) {
if (isInFuture(new Date(license.startsAt))) {
this.activationNotification = {
title: subscriptionActivationFutureDatedNotificationTitle,
message: sprintf(subscriptionActivationFutureDatedNotificationMessage, {
date: license.startsAt,
}),
};
} else {
this.activationNotification = { title: subscriptionActivationNotificationText };
}
},
dismissActivationNotification() {
this.activationNotification = null;
},
},
};
......@@ -99,24 +110,27 @@ export default {
</div>
<hr />
<gl-alert
v-if="shouldShowActivationNotification"
v-if="activationNotification"
variant="success"
:title="$options.i18n.subscriptionActivationNotificationText"
class="mb-4"
:title="activationNotification.title"
class="gl-mb-6"
data-testid="subscription-activation-success-alert"
@dismiss="dismissSuccessAlert"
/>
@dismiss="dismissActivationNotification"
>
{{ activationNotification.message }}
</gl-alert>
<subscription-breakdown
v-if="canShowSubscriptionDetails"
:subscription="currentSubscription"
:subscription-list="subscriptionHistory"
v-on="activationListeners"
/>
<div v-else class="row">
<div class="col-12 col-lg-8 offset-lg-2">
<h3 class="gl-mb-7 gl-mt-6 gl-text-center" data-testid="subscription-activation-title">
{{ $options.i18n.noActiveSubscription }}
</h3>
<subscription-activation-card />
<subscription-activation-card v-on="activationListeners" />
<div class="row gl-mt-7">
<div class="col-lg-6">
<subscription-trial-card />
......
<script>
import { GlCard, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { activateSubscription, howToActivateSubscription, uploadLicenseFile } from '../constants';
import {
activateSubscription,
howToActivateSubscription,
uploadLicenseFile,
SUBSCRIPTION_ACTIVATION_FAILURE_EVENT,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from '../constants';
import SubscriptionActivationErrors from './subscription_activation_errors.vue';
import SubscriptionActivationForm from './subscription_activation_form.vue';
......@@ -30,12 +36,20 @@ export default {
data() {
return {
error: null,
activationListeners: {
[SUBSCRIPTION_ACTIVATION_FAILURE_EVENT]: this.handleActivationFailure,
[SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT]: this.handleActivationSuccess,
},
};
},
methods: {
handleFormActivationFailure(error) {
handleActivationFailure(error) {
this.error = error;
},
handleActivationSuccess(license) {
// Pass on event to parent listeners
this.$emit(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT, license);
},
},
};
</script>
......@@ -62,10 +76,7 @@ export default {
</template>
</gl-sprintf>
</p>
<subscription-activation-form
class="gl-p-5"
@subscription-activation-failure="handleFormActivationFailure"
/>
<subscription-activation-form class="gl-p-5" v-on="activationListeners" />
<template #footer>
<gl-link
v-if="licenseUploadPath"
......
......@@ -13,13 +13,12 @@ import {
activateLabel,
INVALID_CODE_ERROR,
INVALID_CODE_ERROR_MESSAGE,
SUBSCRIPTION_ACTIVATION_FAILURE_EVENT,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
subscriptionActivationForm,
subscriptionQueries,
} from '../constants';
import { getErrorsAsData, updateSubscriptionAppCache } from '../graphql/utils';
export const SUBSCRIPTION_ACTIVATION_FAILURE_EVENT = 'subscription-activation-failure';
export const SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT = 'subscription-activation-success';
import { getErrorsAsData, getLicenseFromData, updateSubscriptionAppCache } from '../graphql/utils';
export default {
name: 'SubscriptionActivationForm',
......@@ -48,7 +47,6 @@ export default {
default: false,
},
},
emits: [SUBSCRIPTION_ACTIVATION_FAILURE_EVENT, SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT],
data() {
const form = {
state: false,
......@@ -81,6 +79,9 @@ export default {
},
},
methods: {
handleError(error) {
this.$emit(SUBSCRIPTION_ACTIVATION_FAILURE_EVENT, error.message);
},
submit() {
if (!this.form.state) {
this.form.showValidation = true;
......@@ -96,21 +97,26 @@ export default {
activationCode: this.form.fields.activationCode.value,
},
},
update: this.updateSubscriptionAppCache,
})
.then((res) => {
update: (cache, res) => {
const errors = getErrorsAsData(res);
if (errors.length) {
const [error] = errors;
if (error.includes(INVALID_CODE_ERROR_MESSAGE)) {
throw new Error(INVALID_CODE_ERROR);
this.handleError(new Error(INVALID_CODE_ERROR));
return;
}
throw new Error(error);
this.handleError(new Error(error));
return;
}
const license = getLicenseFromData(res);
if (license) {
this.$emit(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT, license);
}
this.$emit(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT);
this.updateSubscriptionAppCache(cache, res);
},
})
.catch((error) => {
this.$emit(SUBSCRIPTION_ACTIVATION_FAILURE_EVENT, error.message);
this.handleError(error);
})
.finally(() => {
this.isLoading = false;
......
......@@ -5,6 +5,8 @@ import {
activateLabel,
activateSubscription,
subscriptionActivationInsertCode,
SUBSCRIPTION_ACTIVATION_FAILURE_EVENT,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from '../constants';
import SubscriptionActivationErrors from './subscription_activation_errors.vue';
import SubscriptionActivationForm from './subscription_activation_form.vue';
......@@ -39,14 +41,20 @@ export default {
data() {
return {
error: null,
activationListeners: {
[SUBSCRIPTION_ACTIVATION_FAILURE_EVENT]: this.handleActivationFailure,
[SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT]: this.handleActivationSuccess,
},
};
},
methods: {
handleActivationFailure(error) {
this.error = error;
},
handleActivationSuccess() {
handleActivationSuccess(license) {
this.$emit('change', false);
// Pass on event to parent listeners
this.$emit(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT, license);
},
handleChange(event) {
this.$emit('change', event);
......@@ -77,8 +85,7 @@ export default {
<subscription-activation-form
ref="form"
:hide-submit-button="true"
@subscription-activation-failure="handleActivationFailure"
@subscription-activation-success="handleActivationSuccess"
v-on="activationListeners"
/>
</gl-modal>
</template>
......@@ -156,6 +156,7 @@ export default {
v-if="hasSubscription"
v-model="activationModalVisible"
:modal-id="$options.modal.id"
v-on="$listeners"
/>
<user-callout-dismisser
v-if="canActivateSubscription"
......
......@@ -7,6 +7,12 @@ export const subscriptionMainTitle = s__('SuperSonics|Your subscription');
export const subscriptionActivationNotificationText = s__(
`SuperSonics|Your subscription was successfully activated. You can see the details below.`,
);
export const subscriptionActivationFutureDatedNotificationTitle = s__(
'SuperSonics|Your future dated license was successfully added',
);
export const subscriptionActivationFutureDatedNotificationMessage = s__(
'SuperSonics|You have successfully added a license that activates on %{date}. Please see the subscription history table below for more details.',
);
export const subscriptionActivationInsertCode = __(
"If you've purchased or renewed your subscription and have an activation code, please enter it below to start the activation process.",
);
......@@ -117,6 +123,9 @@ export const buySubscriptionCard = {
buttonLabel: s__('SuperSonics|Buy subscription'),
};
export const SUBSCRIPTION_ACTIVATION_FAILURE_EVENT = 'subscription-activation-failure';
export const SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT = 'subscription-activation-success';
export const INVALID_CODE_ERROR_MESSAGE = 'invalid activation code';
export const CONNECTIVITY_ERROR = 'CONNECTIVITY_ERROR';
export const INVALID_CODE_ERROR = 'INVALID_CODE_ERROR';
......
......@@ -132,15 +132,60 @@ RSpec.describe 'Admin views Subscription', :js do
expect(page).not_to have_link('Export license usage file', href: admin_license_usage_export_path(format: :csv))
end
context 'when activating a subscription fails' do
before do
stub_request(:post, EE::SUBSCRIPTIONS_GRAPHQL_URL)
.to_return(status: 200, body: {
"data": {
"cloudActivationActivate": {
"errors": ["invalid activation code"],
"license": nil
}
}
}.to_json, headers: { 'Content-Type' => 'application/json' })
page.within(find('#content-body', match: :first)) do
fill_activation_form
end
end
it 'shows an error message' do
expect(page).to have_content('An error occurred while activating your subscription.')
end
end
context 'when activating a future-dated subscription' do
before do
license_to_be_created = create(:license, data: create(:gitlab_license, { starts_at: Date.today + 1.month, cloud_licensing_enabled: true, plan: License::ULTIMATE_PLAN }).export)
stub_request(:post, EE::SUBSCRIPTIONS_GRAPHQL_URL)
.to_return(status: 200, body: {
"data": {
"cloudActivationActivate": {
"licenseKey": license_to_be_created.data
}
}
}.to_json, headers: { 'Content-Type' => 'application/json' })
page.within(find('#content-body', match: :first)) do
fill_activation_form
end
end
it 'shows a successful future-dated activation message' do
expect(page).to have_content('Your future dated license was successfully added')
end
end
context 'when activating a new subscription' do
before do
license = create(:license, data: create(:gitlab_license, { cloud_licensing_enabled: true, plan: License::ULTIMATE_PLAN }).export)
license_to_be_created = create(:license, data: create(:gitlab_license, { starts_at: Date.today, cloud_licensing_enabled: true, plan: License::ULTIMATE_PLAN }).export)
stub_request(:post, EE::SUBSCRIPTIONS_GRAPHQL_URL)
.to_return(status: 200, body: {
"data": {
"cloudActivationActivate": {
"licenseKey": license.data
"licenseKey": license_to_be_created.data
}
}
}.to_json, headers: { 'Content-Type' => 'application/json' })
......
......@@ -8,10 +8,13 @@ import SubscriptionBreakdown from 'ee/admin/subscriptions/show/components/subscr
import {
noActiveSubscription,
subscriptionActivationNotificationText,
subscriptionActivationFutureDatedNotificationTitle,
subscriptionHistoryQueries,
subscriptionMainTitle,
subscriptionQueries,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from 'ee/admin/subscriptions/show/constants';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { license, subscriptionHistory } from '../mock_data';
......@@ -20,6 +23,9 @@ const localVue = createLocalVue();
localVue.use(VueApollo);
describe('SubscriptionManagementApp', () => {
// March 16th, 2020
useFakeDate(2021, 2, 16);
let wrapper;
const findActivateSubscriptionCard = () => wrapper.findComponent(SubscriptionActivationCard);
......@@ -99,6 +105,28 @@ describe('SubscriptionManagementApp', () => {
it('does not render the "Export license usage file" link', () => {
expect(findExportLicenseUsageFileLink().exists()).toBe(false);
});
describe('activating the license', () => {
it('shows the activation success notification', async () => {
await findActivateSubscriptionCard().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE,
);
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationNotificationText,
);
});
it('shows the future dated activation success notification', async () => {
await findActivateSubscriptionCard().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE_FUTURE_DATED,
);
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationFutureDatedNotificationTitle,
);
});
});
});
describe('activating the license', () => {
......@@ -122,11 +150,25 @@ describe('SubscriptionManagementApp', () => {
});
});
it('shows the activation success notification', () => {
it('shows the activation success notification', async () => {
await findSubscriptionBreakdown().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE,
);
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationNotificationText,
);
});
it('shows the future dated activation success notification', async () => {
await findSubscriptionBreakdown().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE_FUTURE_DATED,
);
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationFutureDatedNotificationTitle,
);
});
});
describe('with active license', () => {
......
......@@ -4,11 +4,15 @@ import SubscriptionActivationCard, {
activateSubscriptionUrl,
} from 'ee/admin/subscriptions/show/components/subscription_activation_card.vue';
import SubscriptionActivationErrors from 'ee/admin/subscriptions/show/components/subscription_activation_errors.vue';
import SubscriptionActivationForm, {
import SubscriptionActivationForm from 'ee/admin/subscriptions/show/components/subscription_activation_form.vue';
import {
CONNECTIVITY_ERROR,
SUBSCRIPTION_ACTIVATION_FAILURE_EVENT,
} from 'ee/admin/subscriptions/show/components/subscription_activation_form.vue';
import { CONNECTIVITY_ERROR, uploadLicenseFile } from 'ee/admin/subscriptions/show/constants';
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
uploadLicenseFile,
} from 'ee/admin/subscriptions/show/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { license } from '../mock_data';
describe('CloudLicenseApp', () => {
let wrapper;
......@@ -80,6 +84,21 @@ describe('CloudLicenseApp', () => {
expect(findUploadLink().exists()).toBe(false);
});
describe('when the forms emits a success', () => {
beforeEach(() => {
createComponent();
findSubscriptionActivationForm().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE,
);
});
it('passes on the event to the parent component', () => {
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT).length).toBe(1);
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT)[0]).toEqual([license.ULTIMATE]);
});
});
describe('when the forms emits a connectivity error', () => {
beforeEach(() => {
createComponent();
......
import { GlForm, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import SubscriptionActivationForm, {
SUBSCRIPTION_ACTIVATION_FAILURE_EVENT,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from 'ee/admin/subscriptions/show/components/subscription_activation_form.vue';
import SubscriptionActivationForm from 'ee/admin/subscriptions/show/components/subscription_activation_form.vue';
import {
CONNECTIVITY_ERROR,
INVALID_CODE_ERROR,
SUBSCRIPTION_ACTIVATION_FAILURE_EVENT,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
subscriptionQueries,
} from 'ee/admin/subscriptions/show/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -140,7 +139,9 @@ describe('SubscriptionActivationForm', () => {
});
it('emits a successful event', () => {
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT)).toEqual([[]]);
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT)).toEqual([
[activateLicenseMutationResponse.SUCCESS.data.gitlabSubscriptionActivate.license],
]);
});
it('calls the method to update the cache', () => {
......
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SubscriptionActivationErrors from 'ee/admin/subscriptions/show/components/subscription_activation_errors.vue';
import SubscriptionActivationForm, {
SUBSCRIPTION_ACTIVATION_FAILURE_EVENT,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from 'ee/admin/subscriptions/show/components/subscription_activation_form.vue';
import SubscriptionActivationForm from 'ee/admin/subscriptions/show/components/subscription_activation_form.vue';
import SubscriptionActivationModal from 'ee/admin/subscriptions/show/components/subscription_activation_modal.vue';
import {
activateSubscription,
CONNECTIVITY_ERROR,
SUBSCRIPTION_ACTIVATION_FAILURE_EVENT,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
subscriptionActivationInsertCode,
} from 'ee/admin/subscriptions/show/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { preventDefault } from '../../test_helpers';
import { activateLicenseMutationResponse } from '../mock_data';
describe('SubscriptionActivationModal', () => {
let wrapper;
......@@ -106,9 +106,15 @@ describe('SubscriptionActivationModal', () => {
it('hides the modal', () => {
expect(wrapper.emitted('change')).toBeUndefined();
findSubscriptionActivationForm().vm.$emit(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT);
findSubscriptionActivationForm().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
activateLicenseMutationResponse.SUCCESS.data.gitlabSubscriptionActivate.license,
);
expect(wrapper.emitted('change')).toEqual([[false]]);
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT)).toEqual([
[activateLicenseMutationResponse.SUCCESS.data.gitlabSubscriptionActivate.license],
]);
});
});
......
......@@ -17,6 +17,22 @@ export const license = {
usersInLicenseCount: '10',
usersOverLicenseCount: '0',
},
ULTIMATE_FUTURE_DATED: {
activatedAt: '2021-03-16',
billableUsersCount: '8',
expiresAt: '2023-03-16',
company: 'ACME Corp',
email: 'user@acmecorp.com',
id: 'gid://gitlab/License/13',
lastSync: '2021-03-16T00:00:00.000',
maximumUserCount: '8',
name: 'Jane Doe',
plan: 'ultimate',
startsAt: '2022-03-16',
type: subscriptionTypes.CLOUD,
usersInLicenseCount: '10',
usersOverLicenseCount: '0',
},
};
export const subscriptionHistory = [
......
......@@ -32837,9 +32837,15 @@ msgstr ""
msgid "SuperSonics|You do not have an active subscription"
msgstr ""
msgid "SuperSonics|You have successfully added a license that activates on %{date}. Please see the subscription history table below for more details."
msgstr ""
msgid "SuperSonics|You'll be charged for %{trueUpLinkStart}users over license%{trueUpLinkEnd} on a quarterly or annual basis, depending on the terms of your agreement."
msgstr ""
msgid "SuperSonics|Your future dated license was successfully added"
msgstr ""
msgid "SuperSonics|Your subscription"
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