Commit 2b392239 authored by Alex Buijs's avatar Alex Buijs

Merge branch 'growth-87-payment-method-component' into 'growth-87-billing-address-component'

Payment method component for paid signup flow

See merge request gitlab-org/gitlab!21640
parents 954431f3 f106d99b
......@@ -23,6 +23,11 @@ export default {
cycleAnalyticsStagePath: '/-/analytics/cycle_analytics/stages/:stage_id',
cycleAnalyticsDurationChartPath: '/-/analytics/cycle_analytics/stages/:stage_id/duration_chart',
codeReviewAnalyticsPath: '/api/:version/analytics/code_review',
countriesPath: '/-/countries',
countryStatesPath: '/-/country_states',
paymentFormPath: '/-/subscriptions/payment_form',
paymentMethodPath: '/-/subscriptions/payment_method',
confirmOrderPath: '/-/subscriptions',
userSubscription(namespaceId) {
const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId));
......@@ -231,4 +236,29 @@ export default {
const url = Api.buildUrl(this.geoDesignsPath);
return axios.put(`${url}/${projectId}/${action}`, {});
},
fetchCountries() {
const url = Api.buildUrl(this.countriesPath);
return axios.get(url);
},
fetchStates(country) {
const url = Api.buildUrl(this.countryStatesPath);
return axios.get(url, { params: { country } });
},
fetchPaymentFormParams(id) {
const url = Api.buildUrl(this.paymentFormPath);
return axios.get(url, { params: { id } });
},
fetchPaymentMethodDetails(id) {
const url = Api.buildUrl(this.paymentMethodPath);
return axios.get(url, { params: { id } });
},
confirmOrder(params = {}) {
const url = Api.buildUrl(this.confirmOrderPath);
return axios.post(url, params);
},
};
......@@ -3,9 +3,11 @@ import { s__ } from '~/locale';
import ProgressBar from './checkout/progress_bar.vue';
import SubscriptionDetails from './checkout/subscription_details.vue';
import BillingAddress from './checkout/billing_address.vue';
import PaymentMethod from './checkout/payment_method.vue';
import ConfirmOrder from './checkout/confirm_order.vue';
export default {
components: { ProgressBar, SubscriptionDetails, BillingAddress },
components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder },
i18n: {
checkout: s__('Checkout|Checkout'),
},
......@@ -19,6 +21,8 @@ export default {
<h2 class="mt-4 mb-3 mb-lg-5">{{ $options.i18n.checkout }}</h2>
<subscription-details />
<billing-address />
<payment-method />
</div>
<confirm-order />
</div>
</template>
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { ZUORA_SCRIPT_URL, ZUORA_IFRAME_OVERRIDE_PARAMS } from 'ee/subscriptions/new/constants';
export default {
components: {
GlLoadingIcon,
},
props: {
active: {
type: Boolean,
required: true,
},
},
computed: {
...mapState([
'paymentFormParams',
'paymentMethodId',
'creditCardDetails',
'isLoadingPaymentMethod',
]),
},
watch: {
// The Zuora script has loaded and the parameters for rendering the iframe have been fetched.
paymentFormParams() {
this.renderZuoraIframe();
},
},
mounted() {
this.loadZuoraScript();
},
methods: {
...mapActions([
'startLoadingZuoraScript',
'fetchPaymentFormParams',
'zuoraIframeRendered',
'paymentFormSubmitted',
]),
loadZuoraScript() {
this.startLoadingZuoraScript();
if (!window.Z) {
const zuoraScript = document.createElement('script');
zuoraScript.type = 'text/javascript';
zuoraScript.async = true;
zuoraScript.onload = this.fetchPaymentFormParams;
zuoraScript.src = ZUORA_SCRIPT_URL;
document.head.appendChild(zuoraScript);
} else {
this.fetchPaymentFormParams();
}
},
renderZuoraIframe() {
const params = { ...this.paymentFormParams, ...ZUORA_IFRAME_OVERRIDE_PARAMS };
window.Z.runAfterRender(this.zuoraIframeRendered);
window.Z.render(params, {}, this.paymentFormSubmitted);
},
},
};
</script>
<template>
<div>
<gl-loading-icon v-if="isLoadingPaymentMethod" size="lg" />
<div v-show="active && !isLoadingPaymentMethod" id="zuora_payment"></div>
</div>
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlButton,
GlLoadingIcon,
},
computed: {
...mapState(['isConfirmingOrder']),
...mapGetters(['currentStep']),
isActive() {
return this.currentStep === 'confirmOrder';
},
},
methods: {
...mapActions(['confirmOrder']),
},
i18n: {
confirm: s__('Checkout|Confirm purchase'),
confirming: s__('Checkout|Confirming...'),
},
};
</script>
<template>
<div v-if="isActive" class="full-width prepend-bottom-32">
<gl-button :disabled="isConfirmingOrder" variant="success" @click="confirmOrder">
<gl-loading-icon v-if="isConfirmingOrder" inline size="sm" />
{{ isConfirmingOrder ? $options.i18n.confirming : $options.i18n.confirm }}
</gl-button>
</div>
</template>
<script>
import { GlSprintf } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { mapState } from 'vuex';
import Step from './components/step.vue';
import Zuora from './components/zuora.vue';
export default {
components: {
GlSprintf,
Step,
Zuora,
},
computed: {
...mapState(['paymentMethodId', 'creditCardDetails']),
isValid() {
return Boolean(this.paymentMethodId);
},
expirationDate() {
return sprintf(this.$options.i18n.expirationDate, {
expirationMonth: this.creditCardDetails.credit_card_expiration_month,
expirationYear: this.creditCardDetails.credit_card_expiration_year.toString(10).slice(-2),
});
},
},
i18n: {
stepTitle: s__('Checkout|Payment method'),
creditCardDetails: s__('Checkout|%{cardType} ending in %{lastFourDigits}'),
expirationDate: s__('Checkout|Exp %{expirationMonth}/%{expirationYear}'),
},
};
</script>
<template>
<step step="paymentMethod" :title="$options.i18n.stepTitle" :is-valid="isValid">
<template #body="props">
<zuora :active="props.active" />
</template>
<template #summary>
<div class="js-summary-line-1">
<gl-sprintf :message="$options.i18n.creditCardDetails">
<template #cardType>
{{ creditCardDetails.credit_card_type }}
</template>
<template #lastFourDigits>
<strong>{{ creditCardDetails.credit_card_mask_number.slice(-4) }}</strong>
</template>
</gl-sprintf>
</div>
<div class="js-summary-line-2">
{{ expirationDate }}
</div>
</template>
</step>
</template>
export const STEPS = ['subscriptionDetails', 'billingAddress'];
// The order of the steps in this array determines the flow of the application
export const STEPS = ['subscriptionDetails', 'billingAddress', 'paymentMethod', 'confirmOrder'];
export const COUNTRIES_URL = '/-/countries';
export const ZUORA_SCRIPT_URL = 'https://static.zuora.com/Resources/libs/hosted/1.3.1/zuora-min.js';
export const STATES_URL = '/-/country_states';
export const PAYMENT_FORM_ID = 'paid_signup_flow';
export const ZUORA_IFRAME_OVERRIDE_PARAMS = {
style: 'inline',
submitEnabled: 'true',
retainValues: 'true',
};
export const TAX_RATE = 0;
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { sprintf, s__ } from '~/locale';
import createFlash from '~/flash';
import { STEPS, COUNTRIES_URL, STATES_URL } from '../constants';
import Api from 'ee/api';
import { redirectTo } from '~/lib/utils/url_utility';
import { STEPS, PAYMENT_FORM_ID } from '../constants';
export const activateStep = ({ commit }, currentStep) => {
if (STEPS.includes(currentStep)) {
......@@ -36,12 +37,10 @@ export const updateOrganizationName = ({ commit }, organizationName) => {
commit(types.UPDATE_ORGANIZATION_NAME, organizationName);
};
export const fetchCountries = ({ dispatch }) => {
axios
.get(COUNTRIES_URL)
export const fetchCountries = ({ dispatch }) =>
Api.fetchCountries()
.then(({ data }) => dispatch('fetchCountriesSuccess', data))
.catch(() => dispatch('fetchCountriesError'));
};
export const fetchCountriesSuccess = ({ commit }, data = []) => {
const countries = data.map(country => ({ text: country[0], value: country[1] }));
......@@ -60,8 +59,7 @@ export const fetchStates = ({ state, dispatch }) => {
return;
}
axios
.get(STATES_URL, { params: { country: state.country } })
Api.fetchStates(state.country)
.then(({ data }) => dispatch('fetchStatesSuccess', data))
.catch(() => dispatch('fetchStatesError'));
};
......@@ -104,3 +102,98 @@ export const updateCountryState = ({ commit }, countryState) => {
export const updateZipCode = ({ commit }, zipCode) => {
commit(types.UPDATE_ZIP_CODE, zipCode);
};
export const startLoadingZuoraScript = ({ commit }) =>
commit(types.UPDATE_IS_LOADING_PAYMENT_METHOD, true);
export const fetchPaymentFormParams = ({ dispatch }) =>
Api.fetchPaymentFormParams(PAYMENT_FORM_ID)
.then(({ data }) => dispatch('fetchPaymentFormParamsSuccess', data))
.catch(() => dispatch('fetchPaymentFormParamsError'));
export const fetchPaymentFormParamsSuccess = ({ commit }, data) => {
if (data.errors) {
createFlash(
sprintf(s__('Checkout|Credit card form failed to load: %{message}'), {
message: data.errors,
}),
);
} else {
commit(types.UPDATE_PAYMENT_FORM_PARAMS, data);
}
};
export const fetchPaymentFormParamsError = () => {
createFlash(s__('Checkout|Credit card form failed to load. Please try again.'));
};
export const zuoraIframeRendered = ({ commit }) =>
commit(types.UPDATE_IS_LOADING_PAYMENT_METHOD, false);
export const paymentFormSubmitted = ({ dispatch, commit }, response) => {
if (response.success) {
commit(types.UPDATE_IS_LOADING_PAYMENT_METHOD, true);
dispatch('paymentFormSubmittedSuccess', response.refId);
} else {
dispatch('paymentFormSubmittedError', response);
}
};
export const paymentFormSubmittedSuccess = ({ commit, dispatch }, paymentMethodId) => {
commit(types.UPDATE_PAYMENT_METHOD_ID, paymentMethodId);
dispatch('fetchPaymentMethodDetails');
};
export const paymentFormSubmittedError = (_, response) => {
createFlash(
sprintf(
s__(
'Checkout|Submitting the credit card form failed with code %{errorCode}: %{errorMessage}',
),
response,
),
);
};
export const fetchPaymentMethodDetails = ({ state, dispatch, commit }) =>
Api.fetchPaymentMethodDetails(state.paymentMethodId)
.then(({ data }) => dispatch('fetchPaymentMethodDetailsSuccess', data))
.catch(() => dispatch('fetchPaymentMethodDetailsError'))
.finally(() => commit(types.UPDATE_IS_LOADING_PAYMENT_METHOD, false));
export const fetchPaymentMethodDetailsSuccess = ({ commit, dispatch }, creditCardDetails) => {
commit(types.UPDATE_CREDIT_CARD_DETAILS, creditCardDetails);
dispatch('activateNextStep');
};
export const fetchPaymentMethodDetailsError = () => {
createFlash(s__('Checkout|Failed to register credit card. Please try again.'));
};
export const confirmOrder = ({ getters, dispatch, commit }) => {
commit(types.UPDATE_IS_CONFIRMING_ORDER, true);
Api.confirmOrder(getters.confirmOrderParams)
.then(({ data }) => {
if (data.location) dispatch('confirmOrderSuccess', data.location);
else dispatch('confirmOrderError', JSON.stringify(data.errors));
})
.catch(() => dispatch('confirmOrderError'));
};
export const confirmOrderSuccess = (_, location) => {
redirectTo(location);
};
export const confirmOrderError = ({ commit }, message = null) => {
commit(types.UPDATE_IS_CONFIRMING_ORDER, false);
const errorString = message
? s__('Checkout|Failed to confirm your order: %{message}. Please try again.')
: s__('Checkout|Failed to confirm your order! Please try again.');
createFlash(sprintf(errorString, { message }, false));
};
......@@ -15,6 +15,24 @@ export const selectedPlanPrice = (state, getters) =>
export const selectedPlanDetails = state =>
state.availablePlans.find(plan => plan.value === state.selectedPlan);
export const confirmOrderParams = state => ({
setup_for_company: state.isSetupForCompany,
customer: {
country: state.country,
address_1: state.streetAddressLine1,
address_2: state.streetAddressLine2,
city: state.city,
state: state.countryState,
zip_code: state.zipCode,
company: state.organizationName,
},
subscription: {
plan_id: state.selectedPlan,
payment_method_id: state.paymentMethodId,
quantity: state.numberOfUsers,
},
});
export const endDate = state =>
new Date(state.startDate).setFullYear(state.startDate.getFullYear() + 1);
......
......@@ -23,3 +23,13 @@ export const UPDATE_CITY = 'UPDATE_CITY';
export const UPDATE_COUNTRY_STATE = 'UPDATE_COUNTRY_STATE';
export const UPDATE_ZIP_CODE = 'UPDATE_ZIP_CODE';
export const UPDATE_PAYMENT_FORM_PARAMS = 'UPDATE_PAYMENT_FORM_PARAMS';
export const UPDATE_PAYMENT_METHOD_ID = 'UPDATE_PAYMENT_METHOD_ID';
export const UPDATE_CREDIT_CARD_DETAILS = 'UPDATE_CREDIT_CARD_DETAILS';
export const UPDATE_IS_LOADING_PAYMENT_METHOD = 'UPDATE_IS_LOADING_PAYMENT_METHOD';
export const UPDATE_IS_CONFIRMING_ORDER = 'UPDATE_IS_CONFIRMING_ORDER';
......@@ -52,4 +52,24 @@ export default {
[types.UPDATE_ZIP_CODE](state, zipCode) {
state.zipCode = zipCode;
},
[types.UPDATE_PAYMENT_FORM_PARAMS](state, paymentFormParams) {
state.paymentFormParams = paymentFormParams;
},
[types.UPDATE_PAYMENT_METHOD_ID](state, paymentMethodId) {
state.paymentMethodId = paymentMethodId;
},
[types.UPDATE_CREDIT_CARD_DETAILS](state, creditCardDetails) {
state.creditCardDetails = creditCardDetails;
},
[types.UPDATE_IS_LOADING_PAYMENT_METHOD](state, isLoadingPaymentMethod) {
state.isLoadingPaymentMethod = isLoadingPaymentMethod;
},
[types.UPDATE_IS_CONFIRMING_ORDER](state, isConfirmingOrder) {
state.isConfirmingOrder = isConfirmingOrder;
},
};
......@@ -35,6 +35,11 @@ export default ({ planData = '[]', planId, setupForCompany, fullName }) => {
zipCode: null,
countryOptions: [],
stateOptions: [],
paymentFormParams: {},
paymentMethodId: null,
creditCardDetails: {},
isLoadingPaymentMethod: false,
isConfirmingOrder: false,
taxRate: TAX_RATE,
startDate: new Date(Date.now()),
};
......
......@@ -133,6 +133,20 @@ $subscriptions-full-width-lg: 541px;
}
}
#zuora_payment {
margin-right: -8px;
@media(min-width: map-get($grid-breakpoints, lg)) {
margin-right: 0;
}
iframe {
background-color: $white-light;
margin: -4px;
width: 100% !important;
}
}
.order-summary {
max-width: $subscriptions-full-width-md;
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import createStore from 'ee/subscriptions/new/store';
import * as types from 'ee/subscriptions/new/store/mutation_types';
import { GlLoadingIcon } from '@gitlab/ui';
import Component from 'ee/subscriptions/new/components/checkout/components/zuora.vue';
describe('Zuora', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
let store;
let wrapper;
const methodMocks = {
loadZuoraScript: jest.fn(),
renderZuoraIframe: jest.fn(),
};
const createComponent = (props = {}) => {
wrapper = shallowMount(Component, {
propsData: {
active: true,
...props,
},
localVue,
sync: false,
store,
methods: methodMocks,
});
};
const findLoading = () => wrapper.find(GlLoadingIcon);
const findZuoraPayment = () => wrapper.find('#zuora_payment');
beforeEach(() => {
store = createStore();
});
afterEach(() => {
wrapper.destroy();
});
describe('mounted', () => {
it('should call loadZuoraScript', () => {
createComponent();
expect(methodMocks.loadZuoraScript).toHaveBeenCalled();
});
});
describe('when active', () => {
beforeEach(() => {
createComponent();
});
it('does not show the loading icon', () => {
expect(findLoading().exists()).toBe(false);
});
it('the zuora_payment selector should be visible', () => {
expect(findZuoraPayment().element.style.display).toEqual('');
});
describe('when toggling the loading indicator', () => {
beforeEach(() => {
store.commit(types.UPDATE_IS_LOADING_PAYMENT_METHOD, true);
return localVue.nextTick();
});
it('shows the loading icon', () => {
expect(findLoading().exists()).toBe(true);
});
it('the zuora_payment selector should not be visible', () => {
expect(findZuoraPayment().element.style.display).toEqual('none');
});
});
});
describe('when not active', () => {
beforeEach(() => {
createComponent({ active: false });
});
it('does not show loading icon', () => {
expect(findLoading().exists()).toBe(false);
});
it('the zuora_payment selector should not be visible', () => {
expect(findZuoraPayment().element.style.display).toEqual('none');
});
});
describe('renderZuoraIframe', () => {
it('is called when the paymentFormParams are updated', () => {
createComponent();
expect(methodMocks.renderZuoraIframe).not.toHaveBeenCalled();
store.commit(types.UPDATE_PAYMENT_FORM_PARAMS, {});
return localVue.nextTick().then(() => {
expect(methodMocks.renderZuoraIframe).toHaveBeenCalled();
});
});
});
});
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import createStore from 'ee/subscriptions/new/store';
import * as types from 'ee/subscriptions/new/store/mutation_types';
import { GlButton } from '@gitlab/ui';
import Component from 'ee/subscriptions/new/components/checkout/confirm_order.vue';
describe('Confirm Order', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
let wrapper;
const store = createStore();
const createComponent = (opts = {}) => {
wrapper = shallowMount(Component, {
localVue,
store,
...opts,
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('Active', () => {
beforeEach(() => {
store.commit(types.UPDATE_CURRENT_STEP, 'confirmOrder');
});
it('button should be visible', () => {
expect(wrapper.find(GlButton).exists()).toBe(true);
});
});
describe('Inactive', () => {
beforeEach(() => {
store.commit(types.UPDATE_CURRENT_STEP, 'otherStep');
});
it('button should not be visible', () => {
expect(wrapper.find(GlButton).exists()).toBe(false);
});
});
});
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import createStore from 'ee/subscriptions/new/store';
import * as types from 'ee/subscriptions/new/store/mutation_types';
import Step from 'ee/subscriptions/new/components/checkout/components/step.vue';
import Component from 'ee/subscriptions/new/components/checkout/payment_method.vue';
describe('Payment Method', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
let store;
let wrapper;
const createComponent = (opts = {}) => {
wrapper = mount(Component, {
localVue,
store,
...opts,
});
};
beforeEach(() => {
store = createStore();
store.commit(types.UPDATE_PAYMENT_METHOD_ID, 'paymentMethodId');
store.commit(types.UPDATE_CREDIT_CARD_DETAILS, {
credit_card_type: 'Visa',
credit_card_mask_number: '************4242',
credit_card_expiration_month: 12,
credit_card_expiration_year: 2009,
});
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('validations', () => {
const isStepValid = () => wrapper.find(Step).props('isValid');
it('should be valid when paymentMethodId is defined', () => {
expect(isStepValid()).toBe(true);
});
it('should be invalid when paymentMethodId is undefined', () => {
store.commit(types.UPDATE_PAYMENT_METHOD_ID, null);
return localVue.nextTick().then(() => {
expect(isStepValid()).toBe(false);
});
});
});
describe('showing the summary', () => {
it('should show the entered credit card details', () => {
expect(
wrapper
.find('.js-summary-line-1')
.html()
.replace(/\s+/g, ' '),
).toContain('Visa ending in <strong>4242</strong>');
});
it('should show the entered credit card expiration date', () => {
expect(wrapper.find('.js-summary-line-2').text()).toEqual('Exp 12/09');
});
});
});
......@@ -12,6 +12,14 @@ constants.STEPS = ['firstStep', 'secondStep'];
let mock;
describe('Subscriptions Actions', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('activateStep', () => {
it('set the currentStep to the provided value', done => {
testAction(
......@@ -110,14 +118,6 @@ describe('Subscriptions Actions', () => {
});
describe('fetchCountries', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('calls fetchCountriesSuccess with the returned data on success', done => {
mock.onGet(constants.COUNTRIES_URL).replyOnce(200, ['Netherlands', 'NL']);
......@@ -172,14 +172,6 @@ describe('Subscriptions Actions', () => {
});
describe('fetchStates', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('calls resetStates and fetchStatesSuccess with the returned data on success', done => {
mock
.onGet(constants.STATES_URL, { params: { country: 'NL' } })
......@@ -341,4 +333,322 @@ describe('Subscriptions Actions', () => {
);
});
});
describe('startLoadingZuoraScript', () => {
it('updates isLoadingPaymentMethod to true', done => {
testAction(
actions.startLoadingZuoraScript,
undefined,
{},
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: true }],
[],
done,
);
});
});
describe('fetchPaymentFormParams', () => {
it('fetches paymentFormParams and calls fetchPaymentFormParamsSuccess with the returned data on success', done => {
mock
.onGet(constants.PAYMENT_FORM_URL, { params: { id: constants.PAYMENT_FORM_ID } })
.replyOnce(200, { token: 'x' });
testAction(
actions.fetchPaymentFormParams,
null,
{},
[],
[{ type: 'fetchPaymentFormParamsSuccess', payload: { token: 'x' } }],
done,
);
});
it('calls fetchPaymentFormParamsError on error', done => {
mock.onGet(constants.PAYMENT_FORM_URL).replyOnce(500);
testAction(
actions.fetchPaymentFormParams,
null,
{},
[],
[{ type: 'fetchPaymentFormParamsError' }],
done,
);
});
});
describe('fetchPaymentFormParamsSuccess', () => {
it('updates paymentFormParams to the provided value when no errors are present', done => {
testAction(
actions.fetchPaymentFormParamsSuccess,
{ token: 'x' },
{},
[{ type: 'UPDATE_PAYMENT_FORM_PARAMS', payload: { token: 'x' } }],
[],
done,
);
});
it('creates a flash when errors are present', done => {
testAction(
actions.fetchPaymentFormParamsSuccess,
{ errors: 'error message' },
{},
[],
[],
() => {
expect(createFlash).toHaveBeenCalledWith(
'Credit card form failed to load: error message',
);
done();
},
);
});
});
describe('fetchPaymentFormParamsError', () => {
it('creates a flash', done => {
testAction(actions.fetchPaymentFormParamsError, null, {}, [], [], () => {
expect(createFlash).toHaveBeenCalledWith(
'Credit card form failed to load. Please try again.',
);
done();
});
});
});
describe('zuoraIframeRendered', () => {
it('updates isLoadingPaymentMethod to false', done => {
testAction(
actions.zuoraIframeRendered,
undefined,
{},
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: false }],
[],
done,
);
});
});
describe('paymentFormSubmitted', () => {
describe('on success', () => {
it('calls paymentFormSubmittedSuccess with the refID from the response and updates isLoadingPaymentMethod to true', done => {
testAction(
actions.paymentFormSubmitted,
{ success: true, refId: 'id' },
{},
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: true }],
[{ type: 'paymentFormSubmittedSuccess', payload: 'id' }],
done,
);
});
});
describe('on failure', () => {
it('calls paymentFormSubmittedError with the response', done => {
testAction(
actions.paymentFormSubmitted,
{ error: 'foo' },
{},
[],
[{ type: 'paymentFormSubmittedError', payload: { error: 'foo' } }],
done,
);
});
});
});
describe('paymentFormSubmittedSuccess', () => {
it('updates paymentMethodId to the provided value and calls fetchPaymentMethodDetails', done => {
testAction(
actions.paymentFormSubmittedSuccess,
'id',
{},
[{ type: 'UPDATE_PAYMENT_METHOD_ID', payload: 'id' }],
[{ type: 'fetchPaymentMethodDetails' }],
done,
);
});
});
describe('paymentFormSubmittedError', () => {
it('creates a flash', done => {
testAction(
actions.paymentFormSubmittedError,
{ errorCode: 'codeFromResponse', errorMessage: 'messageFromResponse' },
{},
[],
[],
() => {
expect(createFlash).toHaveBeenCalledWith(
'Submitting the credit card form failed with code codeFromResponse: messageFromResponse',
);
done();
},
);
});
});
describe('fetchPaymentMethodDetails', () => {
it('fetches paymentMethodDetails and calls fetchPaymentMethodDetailsSuccess with the returned data on success and updates isLoadingPaymentMethod to false', done => {
mock
.onGet(constants.PAYMENT_METHOD_URL, { params: { id: 'paymentMethodId' } })
.replyOnce(200, { token: 'x' });
testAction(
actions.fetchPaymentMethodDetails,
null,
{ paymentMethodId: 'paymentMethodId' },
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: false }],
[{ type: 'fetchPaymentMethodDetailsSuccess', payload: { token: 'x' } }],
done,
);
});
it('calls fetchPaymentMethodDetailsError on error and updates isLoadingPaymentMethod to false', done => {
mock.onGet(constants.PAYMENT_METHOD_URL).replyOnce(500);
testAction(
actions.fetchPaymentMethodDetails,
null,
{},
[{ type: 'UPDATE_IS_LOADING_PAYMENT_METHOD', payload: false }],
[{ type: 'fetchPaymentMethodDetailsError' }],
done,
);
});
});
describe('fetchPaymentMethodDetailsSuccess', () => {
it('updates creditCardDetails to the provided data and calls activateNextStep', done => {
testAction(
actions.fetchPaymentMethodDetailsSuccess,
{
credit_card_type: 'cc_type',
credit_card_mask_number: '************4242',
credit_card_expiration_month: 12,
credit_card_expiration_year: 2019,
},
{},
[
{
type: 'UPDATE_CREDIT_CARD_DETAILS',
payload: {
credit_card_type: 'cc_type',
credit_card_mask_number: '************4242',
credit_card_expiration_month: 12,
credit_card_expiration_year: 2019,
},
},
],
[{ type: 'activateNextStep' }],
done,
);
});
});
describe('fetchPaymentMethodDetailsError', () => {
it('creates a flash', done => {
testAction(actions.fetchPaymentMethodDetailsError, null, {}, [], [], () => {
expect(createFlash).toHaveBeenCalledWith(
'Failed to register credit card. Please try again.',
);
done();
});
});
});
describe('confirmOrder', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('calls confirmOrderSuccess with a redirect location on success', done => {
mock.onPost(constants.CONFIRM_ORDER_URL).replyOnce(200, { location: 'x' });
testAction(
actions.confirmOrder,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }],
[{ type: 'confirmOrderSuccess', payload: 'x' }],
done,
);
});
it('calls confirmOrderError with the errors on error', done => {
mock.onPost(constants.CONFIRM_ORDER_URL).replyOnce(200, { errors: 'errors' });
testAction(
actions.confirmOrder,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }],
[{ type: 'confirmOrderError', payload: '"errors"' }],
done,
);
});
it('calls confirmOrderError on failure', done => {
mock.onPost(constants.CONFIRM_ORDER_URL).replyOnce(500);
testAction(
actions.confirmOrder,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }],
[{ type: 'confirmOrderError' }],
done,
);
});
});
describe('confirmOrderSuccess', () => {
it('changes the window location', done => {
const spy = jest.spyOn(window.location, 'assign').mockImplementation();
testAction(actions.confirmOrderSuccess, 'http://example.com', {}, [], [], () => {
expect(spy).toHaveBeenCalledWith('http://example.com');
done();
});
});
});
describe('confirmOrderError', () => {
it('creates a flash with a default message when no error given', done => {
testAction(
actions.confirmOrderError,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: false }],
[],
() => {
expect(createFlash).toHaveBeenCalledWith(
'Failed to confirm your order! Please try again.',
);
done();
},
);
});
it('creates a flash with a the error message when an error is given', done => {
testAction(
actions.confirmOrderError,
'"Error"',
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: false }],
[],
() => {
expect(createFlash).toHaveBeenCalledWith(
'Failed to confirm your order: "Error". Please try again.',
);
done();
},
);
});
});
});
......@@ -13,6 +13,15 @@ const state = {
},
],
selectedPlan: 'firstPlan',
country: 'Country',
streetAddressLine1: 'Street address line 1',
streetAddressLine2: 'Street address line 2',
city: 'City',
countryState: 'State',
zipCode: 'Zip code',
organizationName: 'Organization name',
paymentMethodId: 'Payment method ID',
numberOfUsers: 1,
};
describe('Subscriptions Getters', () => {
......@@ -113,4 +122,26 @@ describe('Subscriptions Getters', () => {
expect(getters.usersPresent({ numberOfUsers: 0 })).toBe(false);
});
});
describe('confirmOrderParams', () => {
it('returns the params to confirm the order', () => {
expect(getters.confirmOrderParams(state)).toEqual({
setup_for_company: true,
customer: {
country: 'Country',
address_1: 'Street address line 1',
address_2: 'Street address line 2',
city: 'City',
state: 'State',
zip_code: 'Zip code',
company: 'Organization name',
},
subscription: {
plan_id: 'firstPlan',
payment_method_id: 'Payment method ID',
quantity: 1,
},
});
});
});
});
......@@ -9,6 +9,8 @@ const state = () => ({
organizationName: 'name',
countryOptions: [],
stateOptions: [],
isLoadingPaymentMethod: false,
isConfirmingOrder: false,
});
let stateCopy;
......@@ -19,20 +21,25 @@ beforeEach(() => {
describe('ee/subscriptions/new/store/mutation', () => {
describe.each`
mutation | value | stateProp
${types.UPDATE_CURRENT_STEP} | ${'secondStep'} | ${'currentStep'}
${types.UPDATE_SELECTED_PLAN} | ${'secondPlan'} | ${'selectedPlan'}
${types.UPDATE_IS_SETUP_FOR_COMPANY} | ${false} | ${'isSetupForCompany'}
${types.UPDATE_NUMBER_OF_USERS} | ${2} | ${'numberOfUsers'}
${types.UPDATE_ORGANIZATION_NAME} | ${'new name'} | ${'organizationName'}
${types.UPDATE_COUNTRY_OPTIONS} | ${[{ text: 'country', value: 'id' }]} | ${'countryOptions'}
${types.UPDATE_STATE_OPTIONS} | ${[{ text: 'state', value: 'id' }]} | ${'stateOptions'}
${types.UPDATE_COUNTRY} | ${'NL'} | ${'country'}
${types.UPDATE_STREET_ADDRESS_LINE_ONE} | ${'streetAddressLine1'} | ${'streetAddressLine1'}
${types.UPDATE_STREET_ADDRESS_LINE_TWO} | ${'streetAddressLine2'} | ${'streetAddressLine2'}
${types.UPDATE_CITY} | ${'city'} | ${'city'}
${types.UPDATE_COUNTRY_STATE} | ${'countryState'} | ${'countryState'}
${types.UPDATE_ZIP_CODE} | ${'zipCode'} | ${'zipCode'}
mutation | value | stateProp
${types.UPDATE_CURRENT_STEP} | ${'secondStep'} | ${'currentStep'}
${types.UPDATE_SELECTED_PLAN} | ${'secondPlan'} | ${'selectedPlan'}
${types.UPDATE_IS_SETUP_FOR_COMPANY} | ${false} | ${'isSetupForCompany'}
${types.UPDATE_NUMBER_OF_USERS} | ${2} | ${'numberOfUsers'}
${types.UPDATE_ORGANIZATION_NAME} | ${'new name'} | ${'organizationName'}
${types.UPDATE_COUNTRY_OPTIONS} | ${[{ text: 'country', value: 'id' }]} | ${'countryOptions'}
${types.UPDATE_STATE_OPTIONS} | ${[{ text: 'state', value: 'id' }]} | ${'stateOptions'}
${types.UPDATE_COUNTRY} | ${'NL'} | ${'country'}
${types.UPDATE_STREET_ADDRESS_LINE_ONE} | ${'streetAddressLine1'} | ${'streetAddressLine1'}
${types.UPDATE_STREET_ADDRESS_LINE_TWO} | ${'streetAddressLine2'} | ${'streetAddressLine2'}
${types.UPDATE_CITY} | ${'city'} | ${'city'}
${types.UPDATE_COUNTRY_STATE} | ${'countryState'} | ${'countryState'}
${types.UPDATE_ZIP_CODE} | ${'zipCode'} | ${'zipCode'}
${types.UPDATE_PAYMENT_FORM_PARAMS} | ${{ token: 'x' }} | ${'paymentFormParams'}
${types.UPDATE_PAYMENT_METHOD_ID} | ${'paymentMethodId'} | ${'paymentMethodId'}
${types.UPDATE_CREDIT_CARD_DETAILS} | ${{ type: 'x' }} | ${'creditCardDetails'}
${types.UPDATE_IS_LOADING_PAYMENT_METHOD} | ${true} | ${'isLoadingPaymentMethod'}
${types.UPDATE_IS_CONFIRMING_ORDER} | ${true} | ${'isConfirmingOrder'}
`('$mutation', ({ mutation, value, stateProp }) => {
it(`should set the ${stateProp} to the given value`, () => {
expect(stateCopy[stateProp]).not.toEqual(value);
......
......@@ -137,4 +137,24 @@ describe('projectsSelector default state', () => {
it('sets the startDate to the current date', () => {
expect(state.startDate).toEqual(currentDate);
});
it('sets the paymentFormParams to an empty object', () => {
expect(state.paymentFormParams).toEqual({});
});
it('sets the paymentMethodId to null', () => {
expect(state.paymentMethodId).toBeNull();
});
it('sets the creditCardDetails to an empty object', () => {
expect(state.creditCardDetails).toEqual({});
});
it('sets isLoadingPaymentMethod to false', () => {
expect(state.isLoadingPaymentMethod).toEqual(false);
});
it('sets isConfirmingOrder to false', () => {
expect(state.isConfirmingOrder).toBe(false);
});
});
......@@ -3338,6 +3338,9 @@ msgstr ""
msgid "Checkout|$%{selectedPlanPrice} per user per year"
msgstr ""
msgid "Checkout|%{cardType} ending in %{lastFourDigits}"
msgstr ""
msgid "Checkout|%{name}'s GitLab subscription"
msgstr ""
......@@ -3368,6 +3371,12 @@ msgstr ""
msgid "Checkout|City"
msgstr ""
msgid "Checkout|Confirm purchase"
msgstr ""
msgid "Checkout|Confirming..."
msgstr ""
msgid "Checkout|Continue to billing"
msgstr ""
......@@ -3377,15 +3386,33 @@ msgstr ""
msgid "Checkout|Country"
msgstr ""
msgid "Checkout|Credit card form failed to load. Please try again."
msgstr ""
msgid "Checkout|Credit card form failed to load: %{message}"
msgstr ""
msgid "Checkout|Edit"
msgstr ""
msgid "Checkout|Exp %{expirationMonth}/%{expirationYear}"
msgstr ""
msgid "Checkout|Failed to confirm your order! Please try again."
msgstr ""
msgid "Checkout|Failed to confirm your order: %{message}. Please try again."
msgstr ""
msgid "Checkout|Failed to load countries. Please try again."
msgstr ""
msgid "Checkout|Failed to load states. Please try again."
msgstr ""
msgid "Checkout|Failed to register credit card. Please try again."
msgstr ""
msgid "Checkout|GitLab plan"
msgstr ""
......@@ -3401,6 +3428,9 @@ msgstr ""
msgid "Checkout|Number of users"
msgstr ""
msgid "Checkout|Payment method"
msgstr ""
msgid "Checkout|Please select a country"
msgstr ""
......@@ -3413,6 +3443,9 @@ msgstr ""
msgid "Checkout|Street address"
msgstr ""
msgid "Checkout|Submitting the credit card form failed with code %{errorCode}: %{errorMessage}"
msgstr ""
msgid "Checkout|Subscription details"
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