Skip to content
Merged
70 changes: 70 additions & 0 deletions packages/theme/helpers/__tests__/asyncLocalStorage.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { setItem, getItem, mergeItem, removeItem, clear } from '~/helpers/asyncLocalStorage';

describe('asyncLocalStorage :: Promised Based Localstorage Management', () => {
test('setItem :: should store to localStorage by key', async () => {
const KEY = 'jest'
const VSF_KEY = `vsf-${KEY}`
const VALUE = 'jest-localstorage-value';
const VSF_VALUE = JSON.stringify(VALUE);

await setItem(KEY, VALUE);

expect(localStorage.setItem).toHaveBeenLastCalledWith(VSF_KEY, VSF_VALUE);
expect(localStorage.__STORE__[VSF_KEY]).toBe(VSF_VALUE);
expect(Object.keys(localStorage.__STORE__).length).toBe(1);
});

test('getItem :: should get value from localStorage by key', async () => {
const KEY = 'jest'
const VSF_KEY = `vsf-${KEY}`
const VSF_VALUE = 'jest-localstorage-value';

const VALUE = await getItem(KEY);

expect(localStorage.getItem).toHaveBeenLastCalledWith(VSF_KEY);
expect(localStorage.__STORE__[VSF_KEY]).toBe(JSON.stringify(VALUE));
expect(VSF_VALUE).toBe(VALUE);
});

test('merge :: should merge values to localStorage by key', async () => {
const KEY = 'jest'
const VSF_KEY = `vsf-${KEY}`
const VALUE = {row1: 'Lonely'};
const VSF_VALUE = JSON.stringify(VALUE);
const MERGE_VALUE = {row2: 'Not anymore'};
const VSF_MERGE_VALUE = JSON.stringify({
row1: 'Lonely',
row2: 'Not anymore'
})

await setItem(KEY, VALUE);

expect(localStorage.__STORE__[VSF_KEY]).toBe(VSF_VALUE);
expect(Object.keys(localStorage.__STORE__).length).toBe(1);

await mergeItem(KEY, MERGE_VALUE);

expect(localStorage.__STORE__[VSF_KEY]).toBe(VSF_MERGE_VALUE);
expect(Object.keys(localStorage.__STORE__).length).toBe(1);
});

test('remove :: should remove a key and value from localStorage', async () => {
const KEY = 'jest'
const VSF_KEY = `vsf-${KEY}`

expect(Object.keys(localStorage.__STORE__).length).toBe(1);

await removeItem(KEY);

expect(localStorage.removeItem).toHaveBeenCalledWith(VSF_KEY);
expect(Object.keys(localStorage.__STORE__).length).toBe(0);
});

test('clear :: should clear all values and keys from localStorage', async () => {
await clear();

expect(localStorage.clear).toHaveBeenCalledWith();
expect(Object.keys(localStorage.__STORE__).length).toBe(0);
});
});

42 changes: 42 additions & 0 deletions packages/theme/helpers/asyncLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const getVsfKey = (key) => `vsf-${key}`;

const mergeLocalStorageItem = (key: string, value: string) => {
const oldValue = window.localStorage.getItem(key);
const oldObject = JSON.parse(oldValue);
const newObject = value;
const nextValue = JSON.stringify({ ...JSON.parse(JSON.stringify(oldObject)), ...JSON.parse(JSON.stringify(newObject)) });
window.localStorage.setItem(key, nextValue);
};

const createPromise = (getValue, callback): Promise<any> => new Promise((resolve, reject) => {
try {
const value = getValue();
if (callback) {
callback(null, value);
}
resolve(value);
} catch (err) {
if (callback) {
callback(err);
}
reject(err);
}
});

// eslint-disable-next-line max-len
export const getItem = (key: string, callback?: Function): Promise<any> => createPromise(() => JSON.parse(window.localStorage.getItem(getVsfKey(key))), callback);

// eslint-disable-next-line max-len
export const setItem = (key: string, value: string, callback?: Function): Promise<any> => createPromise(() => (window.localStorage.setItem(getVsfKey(key), JSON.stringify(value))), callback);

// eslint-disable-next-line max-len
export const removeItem = (key: string, callback?: Function): Promise<any> => createPromise(() => {
window.localStorage.removeItem(getVsfKey(key));
}, callback);

// eslint-disable-next-line max-len
export const mergeItem = (key: string, value: string, callback?: Function): Promise<any> => createPromise(() => mergeLocalStorageItem(getVsfKey(key), value), callback);

export const clear = (callback?: Function): Promise<any> => createPromise(() => {
window.localStorage.clear();
}, callback);
26 changes: 26 additions & 0 deletions packages/theme/helpers/checkout/__tests__/steps.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isPreviousStepValid } from '~/helpers/checkout/steps'

const CHECKOUT_DATA = {
'my-account': {a: 'b', c: 'd'}
}

describe('steps :: steps helper for the checkout', () => {

beforeEach(async () => {
localStorage.clear();

localStorage.setItem('vsf-checkout', JSON.stringify(CHECKOUT_DATA))
});

test('user can continue', async () => {
const isValid = await isPreviousStepValid('my-account')

expect(isValid).toBeTruthy();
});

test('user can\'t continue', async () => {
const isValid = await isPreviousStepValid('billing')

expect(isValid).toBeFalsy();
});
});
6 changes: 6 additions & 0 deletions packages/theme/helpers/checkout/steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { getItem } from '~/helpers/asyncLocalStorage';

export const isPreviousStepValid = async (stepToValidate: string) => {
const checkout = await getItem('checkout');
return !(!checkout || !checkout[stepToValidate]);
};
42 changes: 31 additions & 11 deletions packages/theme/pages/Checkout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<SfStep
v-for="(step, key) in STEPS"
:key="key"
:name="step"
:name="$t(step.title)"
:active="1"
can-go-back
>
<nuxt-child />
</SfStep>
Expand Down Expand Up @@ -48,19 +50,37 @@ export default defineComponent({
const { path } = route.value;
const router = useRouter();
const currentStep = computed(() => path.split('/').pop());
const STEPS = ref({
'user-account': 'User Account',
shipping: 'Shipping',
billing: 'Billing',
payment: 'Payment',
});
const currentStepIndex = computed(() => Object.keys(STEPS.value)
.indexOf(currentStep.value));

const STEPS = ref(
[
{
title: 'User Account',
url: 'user-account',
},
{
title: 'Shipping',
url: 'shipping',
},
{
title: 'Billing',
url: 'billing',
},
{
title: 'Payment',
url: 'payment',
},
],
);

const currentStepIndex = computed(() => STEPS.value
.findIndex((step) => step.url === currentStep.value));
const isThankYou = computed(() => currentStep.value === 'thank-you');

const handleStepClick = async (stepIndex) => {
const key = Object.keys(STEPS.value)[stepIndex];
await router.push(`${app.localePath(`/checkout/${key}`)}`);
if (stepIndex <= currentStepIndex.value) {
const { url } = STEPS.value[stepIndex];
await router.push(`${app.localePath(`/checkout/${url}`)}`);
}
};

return {
Expand Down
13 changes: 11 additions & 2 deletions packages/theme/pages/Checkout/Billing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ import {
useContext,
} from '@nuxtjs/composition-api';
import { addressFromApiToForm, formatAddressReturnToData } from '~/helpers/checkout/address';
import { mergeItem } from '~/helpers/asyncLocalStorage';
import { isPreviousStepValid } from '~/helpers/checkout/steps';

const NOT_SELECTED_ADDRESS = '';

Expand Down Expand Up @@ -363,13 +365,14 @@ export default defineComponent({

const handleAddressSubmit = (reset) => async () => {
const addressId = currentAddressId.value;
await save({
const billingDetailsData = {
billingDetails: {
...billingDetails.value,
customerAddressId: addressId,
sameAsShipping: sameAsShipping.value,
},
});
};
await save(billingDetailsData);
if (addressId !== NOT_SELECTED_ADDRESS && setAsDefault.value) {
const chosenAddress = userBillingGetters.getAddresses(
userBilling.value,
Expand All @@ -380,6 +383,7 @@ export default defineComponent({
}
}
reset();
await mergeItem('checkout', { billing: billingDetailsData });
await router.push(`${app.localePath('/checkout/payment')}`);
isBillingDetailsStepCompleted.value = true;
};
Expand Down Expand Up @@ -458,6 +462,11 @@ export default defineComponent({
});

onMounted(async () => {
const validStep = await isPreviousStepValid('shipping');
if (!validStep) {
await router.push(app.localePath('/checkout/user-account'));
}

if (billingDetails.value?.country_code) {
await searchCountry({ id: billingDetails.value.country_code });
}
Expand Down
12 changes: 11 additions & 1 deletion packages/theme/pages/Checkout/Payment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,16 @@ import {
computed,
defineComponent,
useRouter,
useContext,
useContext, onMounted
} from '@nuxtjs/composition-api';
import {
useMakeOrder,
useCart,
cartGetters,
} from '@vue-storefront/magento';
import getShippingMethodPrice from '~/helpers/checkout/getShippingMethodPrice';
import { removeItem } from '~/helpers/asyncLocalStorage';
import { isPreviousStepValid } from '~/helpers/checkout/steps';

export default defineComponent({
name: 'ReviewOrderAndPayment',
Expand Down Expand Up @@ -209,11 +211,19 @@ export default defineComponent({
await load();
});

onMounted(async () => {
const validStep = await isPreviousStepValid('billing');
if (!validStep) {
await router.push(app.localePath('/checkout/user-account'));
}
});

const processOrder = async () => {
await make();
setCart(null);
$magento.config.state.setCartId();
await load();
await removeItem('checkout');
await router.push(`${app.localePath(`/checkout/thank-you?order=${order.value.order_number}`)}`);
};

Expand Down
12 changes: 11 additions & 1 deletion packages/theme/pages/Checkout/Shipping.vue
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ import {
computed,
watch,
onMounted,
defineComponent,
defineComponent, useRouter, useContext,
} from '@nuxtjs/composition-api';
import { onSSR } from '@vue-storefront/core';
import {
Expand All @@ -265,6 +265,8 @@ import {
import { required, min, digits } from 'vee-validate/dist/rules';
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate';
import { addressFromApiToForm } from '~/helpers/checkout/address';
import { mergeItem } from '~/helpers/asyncLocalStorage';
import { isPreviousStepValid } from '~/helpers/checkout/steps';

const NOT_SELECTED_ADDRESS = '';

Expand Down Expand Up @@ -294,6 +296,8 @@ export default defineComponent({
VsfShippingProvider: () => import('~/components/Checkout/VsfShippingProvider.vue'),
},
setup() {
const router = useRouter();
const { app } = useContext();
const {
load,
save,
Expand Down Expand Up @@ -343,6 +347,7 @@ export default defineComponent({
...shippingDetails.value,
customerAddressId: addressId,
};
await mergeItem('checkout', { shipping: shippingDetailsData });
// @TODO remove ignore when https://github.com/vuestorefront/vue-storefront/issues/5967 is applied
// @ts-ignore
await save({ shippingDetails: shippingDetailsData });
Expand Down Expand Up @@ -409,6 +414,11 @@ export default defineComponent({
});

onMounted(async () => {
const validStep = await isPreviousStepValid('user-account');
if (!validStep) {
await router.push(app.localePath('/checkout/user-account'));
}

if (shippingDetails.value?.country_code) {
await searchCountry({ id: shippingDetails.value.country_code });
}
Expand Down
17 changes: 14 additions & 3 deletions packages/theme/pages/Checkout/UserAccount.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,16 @@ import {
computed,
defineComponent,
useRouter,
useContext,
useContext, onMounted,
} from '@nuxtjs/composition-api';
import { useUser, useGuestUser } from '@vue-storefront/magento';
import {
required, min, email,
} from 'vee-validate/dist/rules';
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate';
import { customerPasswordRegExp, invalidPasswordMsg } from '../../helpers/customer/regex';
import { useUiNotification } from '~/composables';
import { getItem, mergeItem } from '~/helpers/asyncLocalStorage';
import { customerPasswordRegExp, invalidPasswordMsg } from '../../helpers/customer/regex';

extend('required', {
...required,
Expand Down Expand Up @@ -234,6 +235,7 @@ export default defineComponent({
}

if (!hasError.value) {
await mergeItem('checkout', { 'user-account': form.value });
await router.push(`${app.localePath('/checkout/shipping')}`);
reset();
isFormSubmitted.value = true;
Expand All @@ -251,14 +253,23 @@ export default defineComponent({

onSSR(async () => {
await load();

if (isAuthenticated.value) {
form.value.firstname = user.value.firstname;
form.value.lastname = user.value.lastname;
form.value.email = user.value.email;
}
});

onMounted(async () => {
const checkout = await getItem('checkout');
if (checkout && checkout['user-account']) {
const data = checkout['user-account'];
form.value.email = data.email;
form.value.firstname = data.firstname;
form.value.lastname = data.lastname;
}
});

return {
canMoveForward,
createUserAccount,
Expand Down
Loading