diff --git a/.github/workflows/conventional-pr-name.yml b/.github/workflows/conventional-pr-name.yml
index 465216619..a52e7a2fd 100644
--- a/.github/workflows/conventional-pr-name.yml
+++ b/.github/workflows/conventional-pr-name.yml
@@ -12,14 +12,13 @@ jobs:
name: Validate PR Title (conventional-commit)
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
-
+ - uses: actions/checkout@v3
- name: Setup node
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: "16"
- name: Install Dependencies
- run: yarn
+ run: rm package.json && npm i @commitlint/config-conventional
- - uses: JulienKode/pull-request-name-linter-action@v0.2.0
+ - uses: JulienKode/pull-request-name-linter-action@v0.5.0
diff --git a/.github/workflows/deploy-vue-storefront-cloud.yml b/.github/workflows/deploy-vue-storefront-cloud.yml
index d98374dbf..014cedd41 100644
--- a/.github/workflows/deploy-vue-storefront-cloud.yml
+++ b/.github/workflows/deploy-vue-storefront-cloud.yml
@@ -8,15 +8,62 @@ on:
- release/*
jobs:
+ create-deployment:
+ runs-on: ubuntu-latest
+ outputs:
+ environment-name: ${{ steps.determine-environment.outputs.name }}
+ environment-code: ${{ steps.determine-environment.outputs.code }}
+ deployment_id: ${{ steps.deployment.outputs.deployment_id }}
+ steps:
+ - name: Determine environment name
+ id: determine-environment
+ shell: bash
+ run: |
+ REF=${{ github.ref }}
+
+ if [ $REF = 'refs/heads/main' ]; then
+ ENVNAME='production'
+ ENVCODE='demo-magento2'
+
+ elif [ $REF = 'refs/heads/develop' ]; then
+ ENVNAME='dev'
+ ENVCODE='demo-magento2-dev'
+
+ elif [[ $REF = refs/heads/release* ]]; then
+ ENVNAME='canary'
+ ENVCODE='demo-magento2-canary'
+
+ elif [ $REF = 'refs/heads/enterprise' ]; then
+ ENVNAME='enterprise'
+ ENVCODE='demo-magento2-enterprise'
+
+ else
+ echo 'unrecognized branch name'
+ exit 1
+ fi
+
+ echo ::set-output name=name::$ENVNAME
+ echo ::set-output name=code::$ENVCODE
+
+ - name: Create GitHub deployment
+ id: deployment
+ uses: chrnorm/deployment-action@v2
+ with:
+ token: ${{ secrets.DEPLOYMENT_PERSONAL_ACCESS_TOKEN }}
+ environment: ${{ steps.determine-environment.outputs.name }}
+ initial-status: in_progress
build:
+ needs: create-deployment
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v1
+
- name: Setup node
uses: actions/setup-node@v1
with:
node-version: 16.x
+
- name: Build and publish docker image
uses: elgohr/Publish-Docker-Github-Action@master
with:
@@ -53,11 +100,10 @@ jobs:
VSF_REDIS_CACHE_INVALIDATE_KEY: magento2vsf2
VSF_REDIS_CACHE_INVALIDATE_URL: /cache-invalidate
- VSF_RECAPTCHA_ENABLED: false
- VSF_RECAPTCHA_HIDE_BADGE: false
+ VSF_RECAPTCHA_ENABLED: true
VSF_RECAPTCHA_VERSION: 3
VSF_RECAPTCHA_SIZE: invisible
- VSF_RECAPTCHA_MIN_SCORE: 0.5
+ VSF_RECAPTCHA_MIN_SCORE: 1.0
VSF_RECAPTCHA_SITE_KEY: 6Ldce0EeAAAAAAGGtGWG-e-SygXiFub6PXHT5fKd
VSF_RECAPTCHA_SECRET_KEY: ${{ secrets.RECAPTCHA_SECRET_KEY }}
@@ -65,54 +111,33 @@ jobs:
LAST_COMMIT: ${{ github.sha }}
- deploy-main:
+ deploy:
+ needs: [create-deployment, build]
uses: ./.github/workflows/deployment-template.yml
- needs: build
- if: github.ref == 'refs/heads/main'
with:
- github_environment_name: production
- environment_code: demo-magento2
- target_url: https://demo-magento2.europe-west1.gcp.storefrontcloud.io
+ environment-code: ${{ needs.create-deployment.outputs.environment-code }}
secrets:
- cloud_username: ${{ secrets.CLOUD_USERNAME }}
- cloud_password: ${{ secrets.CLOUD_PASSWORD }}
- deployment_status_token: ${{ secrets.DEPLOYMENT_PERSONAL_ACCESS_TOKEN }}
+ cloud-username: ${{ secrets.CLOUD_USERNAME }}
+ cloud-password: ${{ secrets.CLOUD_PASSWORD }}
- deploy-develop:
- uses: ./.github/workflows/deployment-template.yml
- needs: build
- if: github.ref == 'refs/heads/develop'
- with:
- github_environment_name: dev
- environment_code: demo-magento2-dev
- target_url: https://demo-magento2-dev.europe-west1.gcp.storefrontcloud.io
- secrets:
- cloud_username: ${{ secrets.CLOUD_USERNAME }}
- cloud_password: ${{ secrets.CLOUD_PASSWORD }}
- deployment_status_token: ${{ secrets.DEPLOYMENT_PERSONAL_ACCESS_TOKEN }}
- deploy-release:
- uses: ./.github/workflows/deployment-template.yml
- needs: build
- if: startsWith(github.ref, 'refs/heads/release')
- with:
- github_environment_name: canary
- environment_code: demo-magento2-canary
- target_url: https://demo-magento2-canary.europe-west1.gcp.storefrontcloud.io
- secrets:
- cloud_username: ${{ secrets.CLOUD_USERNAME }}
- cloud_password: ${{ secrets.CLOUD_PASSWORD }}
- deployment_status_token: ${{ secrets.DEPLOYMENT_PERSONAL_ACCESS_TOKEN }}
+ finalize-deployment:
+ runs-on: ubuntu-latest
+ needs: [create-deployment, build, deploy]
+ if: always()
+ steps:
+ - name: Update deployment status (success)
+ if: ${{ !(contains(join(needs.*.result, ','), 'failure') || contains(join(needs.*.result, ','), 'cancelled')) }}
+ uses: chrnorm/deployment-status@v2
+ with:
+ token: ${{ secrets.DEPLOYMENT_PERSONAL_ACCESS_TOKEN }}
+ deployment-id: ${{ needs.create-deployment.outputs.deployment_id }}
+ state: success
- deploy-enterprise:
- uses: ./.github/workflows/deployment-template.yml
- needs: build
- if: github.ref == 'refs/heads/enterprise'
- with:
- github_environment_name: enterprise
- environment_code: demo-magento2-enterprise
- target_url: https://demo-magento2-enterprise.europe-west1.gcp.storefrontcloud.io
- secrets:
- cloud_username: ${{ secrets.CLOUD_USERNAME }}
- cloud_password: ${{ secrets.CLOUD_PASSWORD }}
- deployment_status_token: ${{ secrets.DEPLOYMENT_PERSONAL_ACCESS_TOKEN }}
+ - name: Update deployment status (failure)
+ if: ${{ contains(join(needs.*.result, ','), 'failure') || contains(join(needs.*.result, ','), 'cancelled') }}
+ uses: chrnorm/deployment-status@v2
+ with:
+ token: ${{ secrets.DEPLOYMENT_PERSONAL_ACCESS_TOKEN }}
+ deployment-id: ${{ needs.create-deployment.outputs.deployment_id }}
+ state: failure
diff --git a/.github/workflows/deployment-template.yml b/.github/workflows/deployment-template.yml
index 73bb98996..ded9778b4 100644
--- a/.github/workflows/deployment-template.yml
+++ b/.github/workflows/deployment-template.yml
@@ -2,42 +2,23 @@
on:
workflow_call:
inputs:
- github_environment_name:
+ environment-code:
required: true
type: string
- environment_code:
- required: true
- type: string
-
- target_url:
- required: true
- type: string
secrets:
- cloud_username:
+ cloud-username:
required: true
- cloud_password:
+ cloud-password:
required: true
- deployment_status_token: # can be ${{ github.token }}
- required: true
-
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- - uses: chrnorm/deployment-action@releases/v1
- name: Create GitHub deployment
- id: deployment
- with:
- token: ${{ secrets.deployment_status_token }}
- environment: ${{ inputs.github_environment_name }}
- initial_status: in_progress
- target_url: ${{ inputs.target_url }}
- - name: Deploy on ${{ inputs.target_url }}
- run: |
- if curl -s -H 'X-User-Id: ${{ secrets.cloud_username }}' -H 'X-Api-Key: ${{ secrets.cloud_password }}' -H 'Content-Type: application/json' -X POST -d '{
- "code":"${{ inputs.environment_code }}",
+ - run: |
+ if curl -s -H 'X-User-Id: ${{ secrets.cloud-username }}' -H 'X-Api-Key: ${{ secrets.cloud-password }}' -H 'Content-Type: application/json' -X POST -d '{
+ "code":"${{ inputs.environment-code }}",
"region":"europe-west1.gcp",
"frontContainerVersion":"${{ github.sha }}"
}' https://farmer.storefrontcloud.io/instances | grep -q '{"code":200,"result":"Instance updated!"}'; then
@@ -47,23 +28,9 @@ jobs:
exit 1
fi
- - name: Update deployment status (success)
- if: success()
- uses: chrnorm/deployment-status@releases/v1
- with:
- token: ${{ secrets.deployment_status_token }}
- target_url: ${{ inputs.target_url }}
- state: success
- description: Congratulations! The deploy is done.
- deployment_id: ${{ steps.deployment.outputs.deployment_id }}
-
- - name: Update deployment status (failure)
- if: failure()
- uses: chrnorm/deployment-status@releases/v1
- with:
- token: ${{ secrets.deployment_status_token }}
- target_url: ${{ inputs.target_url }}
- description: Unfortunately, the instance hasn't been updated.
- state: failure
- deployment_id: ${{ steps.deployment.outputs.deployment_id }}
-
+ # the above curl only *asks* for the container to be deployed
+ # we don't know when the newly built Docker image replaces the old one
+ # but it takes less than 5 minutes
+ - name: 'Wait for container deployed on VSF Cloud to come online'
+ run: 'sleep 300'
+ shell: 'bash'
diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml
index 15e5cefa1..02f658c81 100644
--- a/.github/workflows/publish-npm.yml
+++ b/.github/workflows/publish-npm.yml
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v2
- name: Setup node
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: "16"
registry-url: "https://registry.npmjs.org/"
diff --git a/.github/workflows/speedcurve.yml b/.github/workflows/speedcurve.yml
index eaec33634..817edd7ca 100644
--- a/.github/workflows/speedcurve.yml
+++ b/.github/workflows/speedcurve.yml
@@ -8,10 +8,6 @@ jobs:
if: github.event.deployment_status.state == 'success' && github.event.deployment.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- - name: 'Wait for container deployed on VSF Cloud to come online'
- run: 'sleep 300'
- shell: 'bash'
-
- name: Shorten deployment commit SHA
id: vars
shell: bash
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d1ed60d80..71f1323d2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -25,10 +25,9 @@ jobs:
uses: actions/checkout@v2
- name: Setup node
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: '16.13.0'
- cache: 'yarn'
- name: Install dependencies
run: yarn install
diff --git a/.vuestorefrontcloud/docker/Dockerfile b/.vuestorefrontcloud/docker/Dockerfile
index 918540d3d..3d964fc86 100644
--- a/.vuestorefrontcloud/docker/Dockerfile
+++ b/.vuestorefrontcloud/docker/Dockerfile
@@ -62,7 +62,7 @@ ENV VSF_RECAPTCHA_VERSION=${VSF_RECAPTCHA_VERSION}
ENV VSF_RECAPTCHA_SIZE=${VSF_RECAPTCHA_SIZE}
ENV VSF_RECAPTCHA_MIN_SCORE=${VSF_RECAPTCHA_MIN_SCORE}
ENV VSF_RECAPTCHA_SITE_KEY=${VSF_RECAPTCHA_SITE_KEY}
-ENV VSF_RECAPTCHA_SECRET_KEY=${VSF_RECAPTCHA_SITE_KEY}
+ENV VSF_RECAPTCHA_SECRET_KEY=${VSF_RECAPTCHA_SECRET_KEY}
RUN npm config set @vsf-enterprise:registry=https://registrynpm.storefrontcloud.io
diff --git a/.vuestorefrontcloud/docker/nuxt.config.additional.js b/.vuestorefrontcloud/docker/nuxt.config.additional.js
index b5e52cd90..6fe2b8d3f 100755
--- a/.vuestorefrontcloud/docker/nuxt.config.additional.js
+++ b/.vuestorefrontcloud/docker/nuxt.config.additional.js
@@ -7,7 +7,7 @@ export default () => {
...baseDefaults,
modules: [
...baseDefaults.modules,
- '@nuxtjs/sentry'
+ '@nuxtjs/sentry',
],
sentry: {
dsn: process.env.VSF_SENTRY_DSN,
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 8d0efcddc..dde7c0a95 100755
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -63,6 +63,18 @@ module.exports = {
'@vuepress/active-header-links',
'@vuepress/search',
],
+
+ /**
+ * Ref: https://v1.vuepress.vuejs.org/config/#markdown
+ */
+ markdown: {
+ extendMarkdown: md => {
+ md.use(require('markdown-it-video'), {
+ youtube: { width: 740, height: 416.25 }, // 16:9 ratio, where 740px is the width of the page content
+ });
+ }
+ },
+
themeConfig: {
GTM_TAG,
sidebarDepth: 0,
@@ -91,14 +103,26 @@ module.exports = {
],
},
{
- title: 'Getting started',
+ title: 'Installation & Setup',
collapsable: false,
children: [
- ['/getting-started/installation', 'Installation'],
- ['/getting-started/configure-magento', 'Configuring Magento'],
- ['/getting-started/configure-integration', 'Configuring Vue Storefront'],
+ ['/installation-setup/installation', 'Installation'],
+ ['/installation-setup/configure-magento', 'Configuring Magento'],
+ ['/installation-setup/configure-integration', 'Configuring Vue Storefront'],
],
},
+ {
+ title: 'Getting started',
+ collapsable: false,
+ children: [
+ ['/getting-started/introduction', 'Introduction'],
+ ['/getting-started/project-structure', 'Project structure'],
+ ['/getting-started/configuration', 'Configuration'],
+ ['/getting-started/layouts-and-routing', 'Layouts and Routing'],
+ ['/getting-started/theme', 'Theme'],
+ ['/getting-started/internationalization', 'Internationalization']
+ ]
+ },
{
title: 'Composition',
collapsable: false,
diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl
index 9cf26251a..99ab9ae94 100644
--- a/docs/.vuepress/styles/index.styl
+++ b/docs/.vuepress/styles/index.styl
@@ -26,7 +26,46 @@ a code {
font-weight: bold;
}
-div.theme-default-content:not(.custom) {
- max-width: 1024px;
+q {
+ position: relative;
+ display: block;
+ margin: 30px 0;
+ padding: 40px 20px;
+ border-radius: 8px;
}
+@media (max-width: 959px) {
+ q {
+ margin: 20px 0;
+ padding: 40px 0;
+ }
+}
+
+q::before,
+q::after {
+ position: absolute;
+ font-size: 40px;
+ line-height: 1;
+ color: #9CA3AF;
+}
+
+
+q::before {
+ content: '❝';
+ top: 5px;
+ left: 0;
+}
+
+q::after {
+ content: '❞';
+ bottom: -5px;
+ right: 0;
+}
+
+q p {
+ margin: 0px;
+ font-size: 1.1rem;
+ font-weight: 500;
+ font-style: italic;
+ text-align: center;
+}
diff --git a/docs/assets/images/storefront-ui.webp b/docs/assets/images/storefront-ui.webp
new file mode 100644
index 000000000..30e78c1f0
Binary files /dev/null and b/docs/assets/images/storefront-ui.webp differ
diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md
new file mode 100644
index 000000000..f12b1f114
--- /dev/null
+++ b/docs/getting-started/configuration.md
@@ -0,0 +1,37 @@
+# Configuration
+
+Whenever starting a new project or jumping into an existing one, you should look into the configuration files first. They control almost every aspect of the application, including installed integrations.
+
+## Mandatory configuration files
+
+Every Vue Storefront project must contain two configuration files described below. They control both client and server parts of the application.
+
+### `nuxt.config.js`
+
+**The `nuxt.config.js` file is the starting point of every project.** It contains general configuration, including routes, global middlewares, internationalization, or build information. This file also registers modules and plugins to add or extend framework features. Because almost every Vue Storefront integration has a module or plugin, you can identify which integrations are used by looking at this file.
+
+You can learn more about this file and available configuration options on the [Nuxt configuration file](https://nuxtjs.org/docs/directory-structure/nuxt-config/) page.
+
+### `middleware.config.js`
+
+The `middleware.config.js` file is as essential as `nuxt.config.js`, but much simpler and likely smaller. It configures the Server Middleware used for communication with e-commerce platforms and contains sensitive credentials, custom endpoints, queries, etc.
+
+### `.env`
+
+The `.env` file contains environmental variables used in both `nuxt.config.js` and `middleware.config.js` files for configuration that differs per environment. New projects come with a `.env.example` that you can rename (or clone) to the `.env` file and configure.
+
+## Optional configuration files
+
+Your project might include some additional configuration files not described above. Let's walk through the most common ones.
+
+- [`tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) configures compiler used to strongly type the project using TypeScript language. It defines a list of files and directories to be included and excluded from analysis and compiling.
+
+- [`babel.config.js`](https://babeljs.io/docs/en/config-files) configures Babel compiler used to make the code backward compatible with older browsers and environments.
+
+- [`ecosystem.config.js`](https://pm2.keymetrics.io/docs/usage/application-declaration/) configures PM2 Process Management that runs and manages Node application.
+
+- [`jest.config.js`](https://jestjs.io/docs/configuration) configures Jest testing framework, used for writing and running unit tests.
+
+## What's next
+
+As the next step, we will learn about [Layouts and Routing](./layouts-and-routing.html) and show what routes come predefined in every Vue Storefront project and how to register custom ones.
diff --git a/docs/getting-started/internationalization.md b/docs/getting-started/internationalization.md
new file mode 100644
index 000000000..97a6638d5
--- /dev/null
+++ b/docs/getting-started/internationalization.md
@@ -0,0 +1,158 @@
+# Internationalization
+
+If you're building a shop for an international brand, you likely want it translated to different languages and using different currencies. In this document, you will learn how we're approaching internationalization in Vue Storefront and how to configure your application to use it.
+
+::: warning i18n is not multi-tenancy!
+This document only explains how to make a single shop instance available for multiple countries. If you need to build a system for multiple tenants, we suggest creating an instance of Vue Storefront for each tenant and sharing common resources through an NPM package.
+:::
+
+## How it works by default?
+
+By default, we are using the [`nuxt-i18n`](https://i18n.nuxtjs.org/) module for handling both translations and currencies. It's preinstalled in the [default theme](/getting-started/theme.html#what-s-makes-a-default-theme) and is configured for English and German translations out of the box.
+
+The `nuxt-i18n` module adds the `$t('key')` and `$n(number)` helpers to translate strings and format the currencies.
+
+You can find the translation keys in the `lang` directory of your project and configuration for currencies in `nuxt.config.js`.
+
+::: tip
+Even though the module is included in the default theme, it's not required. You can [disable it](#configuring-modules-separately) and handle internationalization yourself.
+:::
+
+To provide a unified way to configure internationalization across the application for different modules and integrations, we have introduced the `i18n` field in each module's configuration. It has the same format as the `nuxt-i18n` options. You can add any configuration there, and it will be propagated to all other Vue Storefront modules.
+
+All Vue Storefront integrations have the `useNuxtI18nConfig` property set to `true`. It means that they will use the same configuration as you provided for `nuxt-i18n` in the `i18n` field of your `nuxt.config.js`.
+
+```js
+// nuxt.config.js
+
+export default {
+ buildModules: [
+ [
+ '@vue-storefront/{INTEGRATION}/nuxt',
+ {
+ // other comfiguration options
+ i18n: {
+ useNuxtI18nConfig: true,
+ },
+ },
+ ],
+ ],
+ i18n: {
+ locales: [
+ {
+ code: 'en',
+ label: 'English',
+ file: 'en.js',
+ iso: 'en',
+ },
+ {
+ code: 'de',
+ label: 'German',
+ file: 'de.js',
+ iso: 'de',
+ },
+ ],
+ defaultLocale: 'en',
+ },
+};
+```
+
+## Configuring modules separately
+
+You can provide your own i18n configuration for a specific module by setting `useNuxtI18nConfig` to `false`.
+
+```js
+// nuxt.config.js
+
+export default {
+ [
+ '@vue-storefront/{INTEGRATION}/nuxt',
+ {
+ i18n: {
+ useNuxtI18nConfig: false,
+ locales: [
+ {
+ code: 'en',
+ label: 'English',
+ file: 'en.js',
+ iso: 'en',
+ },
+ {
+ code: 'de',
+ label: 'German',
+ file: 'de.js',
+ iso: 'de',
+ },
+ ],
+ defaultLocale: 'en'
+ }
+ }
+ ];
+}
+```
+
+## CDN and cookies
+
+The server's response cannot contain the `Set-Cookie` header to make CDNs cache the website correctly.
+
+To achieve that, we've made our own mechanism for handling cookies responsible for storing `locale`, `currency`, and `country`. This mechanism also handles language detection and redirecting to the proper locale. For this reason, `@nuxtjs/i18n` language detection is disabled by default.
+
+```js
+// nuxt.config.js
+export default {
+ i18n: {
+ detectBrowserLanguage: false,
+ },
+};
+```
+
+If you don't want to redirect users, you can disable this mechanism, as described in the section below.
+
+## Disabling the auto-redirect mechanism
+
+The `@vue-storefront/nuxt` module includes an auto-redirect mechanism that performs a server-side redirect to different URLs based on the target locale.
+
+You can disable this by setting `autoRedirectByLocale` to `false` in the `i18n` configuration object.
+
+```js
+// nuxt.config.js
+
+export default {
+ i18n: {
+ autoRedirectByLocale: false,
+ },
+};
+```
+
+## Currency detection
+
+In addition to language detection, we also set currency based on locale. You can configure it in `nuxt.config.js` with the `vueI18n.numberFormats` property.
+
+For more configuration options of `numberFormats` entries, check the [Intl.NumberFormat()](https://developer.mozilla.org/en-US/docs/web/javascript/reference/global_objects/intl/numberformat) documentation.
+
+```js
+// nuxt.config.js
+
+export default {
+ i18n: {
+ vueI18n: {
+ numberFormats: {
+ en: {
+ currency: {
+ style: 'currency',
+ currency: 'USD',
+ currencyDisplay: 'symbol',
+ },
+ },
+ // ...other locales
+ },
+ },
+ },
+};
+```
+
+**Please note that only one currency can be set for each locale.**
+
+If this is a limitation for you, or if you don't want to have currencies tied to locales, you can disable this mechanism and provide your own.
+
+To disable it, set `autoChangeCookie.currency` to `false` as described in the section below.
diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md
new file mode 100644
index 000000000..55386a17e
--- /dev/null
+++ b/docs/getting-started/introduction.md
@@ -0,0 +1,49 @@
+# Introduction to Vue Storefront
+
+Without a proper understanding of the framework you're using, you might spend weeks or even months doing something that someone else has already done. That's why before we can dive deep into the project itself, we need to understand what powers it all under the hood.
+
+@[youtube](MCN1rRwuIGs)
+
+## It's all Nuxt.js
+
+We didn't want to reinvent the wheel and introduce yet another framework that solves the same issues as its predecessors, which took them years to mature. That's why...
+
+
+
+Vue Storefront is essentially a [Nuxt.js](https://nuxtjs.org/) project with some plugins and modules preinstalled, as well as a ready-to-use e-commerce theme. Nuxt.js handles most of the front-end work and [Server Side Rendering](https://nuxtjs.org/docs/concepts/server-side-rendering/), while Vue Storefront adds the e-commerce specific bits and integrations to various platforms.
+
+
+
+Some of the plugins and modules that come with the fresh installation were created by the Nuxt.js community, and others come from our core team specifically for Vue Storefront projects.
+
+The default theme mainly consists of components from the [Storefront UI](http://storefrontui.io/) — an e-commerce focused UI library maintained by the Vue Storefront team.
+
+We described plugins, modules, and the theme in more detail on the following pages.
+
+### But why?
+
+You might be wondering why we choose Nuxt.js as our foundation. After all, it wasn't created with e-commerce in mind.
+
+When you start using any new framework, you expect it to:
+
+* have **plugins that solve common issues**, such as analytics, SEO, internationalization, etc.,
+* be **versatile and flexible** enough to allow extending it and creating custom integrations,
+* have **active and diverse community**, which answers questions, writes articles, and promotes the framework.
+
+Creating such an ecosystem from scratch takes years. But because we based Vue Storefront on Nuxt.js — the biggest Vue.js framework — it ticks all the boxes. Nuxt.js has a vast library of [ready-to-use modules](https://modules.nuxtjs.org/) and an active community of thousands of developers on the [Nuxt.js Discord server](https://discord.com/invite/ps2h6QT). It's also flexible enough to make it e-commerce ready with just a few plugins and modules.
+
+
+
+Combining general-purpose modules from Nuxt.js and e-commerce specific modules from Vue Storefront significantly shortens the time-to-market. It allows you to focus on what's specific to your project.
+
+
+
+## Start with Vue.js
+
+If you are new to the Vue.js ecosystem, the best place to start learning is the [Vue.js 2 documentation](https://v2.vuejs.org/). **In our documentation, we assume prior knowledge of Vue.js (with some exceptions)**. While we do our best to explain each topic in detail, a lack of this knowledge might cause you to get lost.
+
+We also encourage reading [Nuxt.js 2 documentation](https://nuxtjs.org/docs/). In most places, we don't assume this knowledge and add links to related documents, but having it will make you more confident and let you work faster and more efficiently.
+
+## What's next
+
+Now that you understand what Vue Storefront is, it's time to understand the [Project structure](./project-structure.html) and learn how to navigate it and which files do what.
diff --git a/docs/getting-started/layouts-and-routing.md b/docs/getting-started/layouts-and-routing.md
new file mode 100644
index 000000000..a4515ba4e
--- /dev/null
+++ b/docs/getting-started/layouts-and-routing.md
@@ -0,0 +1,145 @@
+# Layouts and Routing
+
+Layouts and Routing in Vue Storefront are entirely powered by Nuxt.js. We will only give a brief overview of these features on this page, but you can learn more on the [Views](https://nuxtjs.org/docs/concepts/views/) page in the Nuxt.js documentation.
+
+## View hierarchy
+
+Every page consists of layers — some layers are shared between many routes, others are used in one specific route. We described each layer in detail in the sections below.
+
+### HTML document
+
+At the root of the hierarchy is the HTML document. By default, the framework provides one, but you can customize it by creating an `app.html` file in the root directory of your project. It has special tags used to insert parts of the documents, and by default, looks like this:
+
+```html
+
+
+
+ {{ HEAD }}
+
+
+ {{ APP }}
+
+
+```
+
+### Layouts
+
+Nuxt.js automatically registers all `.vue` files inside of the `layouts` directory as layouts. Unless configured otherwise, pages use the `default.vue` component as their layout. Open the `layouts/default.vue` component to get the general idea of how the page looks like, what components it imports and what data it loads within the `setup` function.
+
+Layout components include a special ` ` component that displays the page's content based on the current URL. Those pages are Vue.js components, too, explained in the next section.
+
+There is also an `error.vue` layout — used when an error occurs. It doesn't use the ` ` component but receives the `error` prop, which you can use to get an error code or message.
+
+The convention is to use `lowercase` when naming components inside of the `layouts` directory, e.g., `blog.vue`.
+
+### Pages
+
+Nuxt.js automatically registers all `.vue` files inside the `pages` directory as application routes. For example, creating an `AboutUs.vue` component will create an `/aboutus` route. In the same way, creating this component inside a nested directory called `company` will create the `/company/aboutus` route. It's all thanks to the [File System Routing](https://nuxtjs.org/docs/2.x/features/file-system-routing/) feature available in Nuxt.js. In the sections below, we describe how to modify routes manually.
+
+Pages can define a custom [layout](https://nuxtjs.org/docs/directory-structure/pages#layout) property to change the default used for this view.
+
+The convention is to use `PascalCase` when naming components inside of the `pages` directory, e.g., `MyAccount.vue`.
+
+## Manually adding and modifying routes
+
+If you want to manually add your custom routes or modify some already provided, use the `extendRoutes` function in the `nuxt.config.js`. This function has two properties:
+
+* `routes` — an array of already registered routes. You can `push` or delete entries from it.
+* `resolve` — helper function for resolving Vue.js components based on their paths in the project.
+
+For the sake of example, let's assume that we created a `pages/AboutUs.vue` component, but we want to use the `/company/about-us` route instead of auto-registered `/aboutus`. There are two approaches we could take.
+
+The first approach is to **delete existing route** and **register new route** with a different path.
+
+```javascript
+// nuxt.config.js
+
+export default {
+ router: {
+ extendRoutes(routes, resolve) {
+ // Delete automatically registered route
+ routes.splice(
+ routes.findIndex(route => route.path === '/AboutUs'),
+ 1
+ );
+
+ // Re-register the same component but with different path
+ routes.push({
+ name: 'AboutUs',
+ path: '/company/about-us',
+ component: resolve(__dirname, 'pages/AboutUs.vue')
+ });
+ }
+ }
+};
+```
+
+Alternatively, we can **modify the `path` property of the existing entry** like so:
+
+```javascript
+// nuxt.config.js
+
+export default {
+ router: {
+ extendRoutes(routes, resolve) {
+ // Find route index
+ const index = routes.findIndex(route => route.path === '/AboutUs');
+
+ // Modify route path
+ routes[index].path = '/company/about-us';
+ }
+ }
+};
+```
+
+## Changing base path
+
+There are cases when your store is served under a specific path, eg. `example.com/shop/`. To make Vue.js router aware of this, you need to update the configuration in the `nuxt.config.js`:
+
+```javascript
+// nuxt.config.js
+
+export default {
+ router: {
+ base: "/shop/"
+ }
+};
+```
+
+Unfortunately not all links in your application will detect this. You can fix it, by wrap all relative links and paths to assets in `addBasePath` helper.
+
+```vue
+
+
+
+
+
+
+```
+
+## Navigating between pages
+
+To navigate between pages within your application, use the [NuxtLink](https://nuxtjs.org/docs/features/nuxt-components/#the-nuxtlink-component) component, instead of the traditional `` tag. While you can use the ` ` tag for external links, using the ` ` for internal links will ensure that you make use of the Single-Page Navigation capabilities that Nuxt.js provides.
+
+Single-Page Navigation (SPA) provides many benefits, such as:
+
+* much faster and more responsive navigation compared to the traditional server navigation,
+* framework is initialized only once,
+* components shared between the pages are rendered only once (if they use the same layout),
+* fewer requests and data sent over the network.
+
+## What's next
+
+Layouts and pages are one thing, but in the end, they must display components and styles. Otherwise, we would only serve blank pages.
+
+New projects come with a default [Theme](./theme.html), so the next step is to understand what makes it look like this.
diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md
new file mode 100644
index 000000000..c9a109ca4
--- /dev/null
+++ b/docs/getting-started/project-structure.md
@@ -0,0 +1,45 @@
+# Project structure
+
+If you followed our [Installation](/installation-setup/installation.html) guide, you should have a Vue Storefront project with some Magento already integrated. This project has a bunch of directories, Vue components, and files, but what does what?
+
+In this section, you will learn how to navigate the Vue Storefront project and where to start developing.
+
+## Structure basics
+
+As described on the [Introduction to Vue Storefront](./introduction.html) page, Vue Storefront is just a Nuxt.js project under the hood. For this reason, our project structure inherits from Nuxt.js but has some additional files.
+
+To learn about it in-depth, you can refer to the [Directory Structure in Nuxt.js project](https://nuxtjs.org/docs/get-started/directory-structure/) document, but the gist of it is:
+
+* [**.nuxt**](https://nuxtjs.org/docs/2.x/directory-structure/nuxt) is a dynamically generated build directory. You should **not** manually modify it, nor synchronize it using version control like GIT.
+
+* [**assets**](https://nuxtjs.org/docs/2.x/directory-structure/assets) contains uncompiled assets such as your styles, images, or fonts.
+
+* [**components**](https://nuxtjs.org/docs/2.x/directory-structure/components) contains Vue.js components used on different pages or parts of your application. You can import these components from pages, layouts, and other components.
+
+* **composables** provides business logic that can be used in any place in the app.
+
+* **getters** are functions that help to get commonly used data.
+
+* **helpers** are utility functions that help resolve some common tasks like for example format currency
+
+* [**lang**](https://docs.vuestorefront.io/v2/getting-started/internationalization.html) contains translations for your application. Available locales are configured in the `nuxt.config.js` file.
+
+* [**layouts**](https://nuxtjs.org/docs/2.x/directory-structure/layouts) contains Vue.js components that act as a UI base for the whole application or specific pages.
+
+* [**middleware**](https://nuxtjs.org/docs/2.x/directory-structure/middleware) contains JavaScript files that contain custom functions run before rendering a whole application or just a specific layout or page. These can be used, for example, to protect pages from unauthorized access or redirect if some conditions are not met.
+
+* **modules** contains code related to specific area of the app like customer, checkout and so on. Each module can have components, helpers, composables and other files and services
+
+* [**pages**](https://nuxtjs.org/docs/2.x/directory-structure/pages) contains Vue.js components that Nuxt.js automatically registers as routes.
+
+* [**static**](https://nuxtjs.org/docs/2.x/directory-structure/static) contains files that likely won't change, such as favicon, `robots.txt`, sitemap, or company logos.
+
+* **test-utils** contains utilities and helpers for unit testing.
+
+* **types** provides global TypeScript types
+
+* **middleware.config.js** and **nuxt.config.js** configurations file are described in detail in the [Configuration](./configuration.html) document.
+
+## What's next
+
+With a basic understanding of the project structure, it's time to learn about the [Configuration](./configuration.html) files that control the application and installed integrations. They are a crucial part of every Vue Storefront application.
diff --git a/docs/getting-started/theme.md b/docs/getting-started/theme.md
new file mode 100644
index 000000000..bb108c12f
--- /dev/null
+++ b/docs/getting-started/theme.md
@@ -0,0 +1,109 @@
+# Theme
+
+A project without any theme would just show a blank page, but if you have seen any of Vue Storefront demos or created a project using our CLI, you know that's not the case. So what makes it look like it does?
+
+This page will describe what makes the default theme, how to customize it, and what tricks we use to improve the performance.
+
+## What's makes a default theme
+
+### Preinstalled modules and libraries
+
+Every new Vue Storefront project comes with a set of preinstalled Nuxt.js modules and plugins, as well as Vue.js libraries. These packages offer a variety of features from cookie handling to form validation and are used by the base theme. You can remove some of them, but only if you decide to create a custom theme from scratch.
+
+#### Nuxt.js modules and plugins
+
+- [`@nuxt/typescript-build`](https://typescript.nuxtjs.org/) - for TypeScript support,
+
+- [`@nuxtjs/pwa`](https://pwa.nuxtjs.org/) - for PWA functionalities,
+
+- [`@nuxtjs/composition-api`](https://composition-api.nuxtjs.org/) - for Composition API support,
+
+- [`@nuxtjs/style-resources`](https://www.npmjs.com/package/@nuxtjs/style-resources) - for importing SASS variables globally,
+
+- [`nuxt-i18n`](https://i18n-legacy.nuxtjs.org/) - for internationalization (translations and price formatting),
+
+- [`nuxt-purgecss`](https://purgecss.com/guides/nuxt.html) - for removing unused CSS from the final build,
+
+- [`cookie-universal-nuxt`](https://www.npmjs.com/package/cookie-universal-nuxt) - for handling cookies on the server (SSR) and client (browser).
+
+#### Vue.js libraries
+
+- [`vee-validate`](https://vee-validate.logaretm.com/v3) - for frontend form validation,
+
+- [`vue-scrollto/nuxt`](https://www.npmjs.com/package/vue-scrollto) - for smooth scrolling to HTML elements,
+
+- [`vue-lazy-hydration`](https://www.npmjs.com/package/vue-lazy-hydration) - for delaying hydration and improving performance.
+
+### Storefront UI
+
+
+
+ (Click to zoom)
+
+
+Almost every page in our default theme uses components whose names start with `Sf`. These come from the [Storefront UI](http://storefrontui.io/) — a design system and library of Vue.js components dedicated to e-commerce, maintained by the Vue Storefront team. Every component can be heavily customized using [props](https://v2.vuejs.org/v2/guide/components-props.html) and [slots](https://v2.vuejs.org/v2/guide/components-slots.html).
+
+Please check [Storefront UI documentation](https://docs.storefrontui.io/) to learn more and interactively customize and test the components.
+
+::: tip Want to use another UI library? No problem!
+If you don't want to use Storefront UI, feel free to remove it from your project. It's just a UI layer, and the project can work with any other UI library or a custom code.
+:::
+
+## How to customize the theme
+
+Every default theme will need customization at some point. Regardless of how complex the changes are, we recommend reusing as much from the default theme as possible. This will not only save you time but will likely reduce the number of bugs, thanks to the time we spend on stabilization and testing.
+
+### Updating layouts, pages, and components
+
+To update the existing component, you need to identify it first, and Vue.js Devtools is invaluable in this. Open the Vue.js Devtools, right-click the DOM element you want to update, and select `Inspect Vue component`. One of the components in the tree in Vue.js Devtools should get highlighted. You can look for the component with the same name in the `layout`, `pages`, or `components` directories and update it to your needs. However, there are a few exceptions to this rule described below.
+
+#### `Sf` components
+
+If the component's name starts with `Sf`, it means that it comes from [StorefrontUI](https://storefrontui.io/) library. The look and behavior of such components are controlled using props and slots passed from the direct **parent** component.
+
+#### `LazyHydrate` and `Anonymous Component` components
+
+These two components come from the [vue-lazy-hydration](https://github.com/maoberlehner/vue-lazy-hydration) library and are wrappers around other components. They are used to improve the performance by deferring the hydration process (making components interactive) and don't affect the look of other components. The behavior of such components is controlled using props passed from the direct **parent** component.
+
+### Updating styles
+
+There are a few ways of updating the default styles. Below we describe the most optimal methods for the most common cases.
+
+#### Adding global styleheet
+
+To add global styles applied to all pages, use the [css property](https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-css/) in `nuxt.config.js`.
+
+#### Adding stylesheet to specific layout, page, or component
+
+To add a stylesheet to a specific component, use `@import` regardless of whether you are using CSS, SCSS, or LESS.
+
+```vue
+
+```
+
+#### Using variables, mixins, and function in components
+
+Usually, to access style variables, mixins, and functions, we import them in every component separately. Thanks to the [@nuxtjs/style-resources](https://github.com/nuxt-community/style-resources-module#readme) module, we can register them in `nuxt.config.js` and access them without extra `@import` statements.
+
+:::danger Be careful
+Stylesheets in `styleResources` should **only** contain variables, mixins, and functions. During the build process, the components import these stylesheets. Any **styles** declared in them are added to every component, significantly impacting the performance and application size.
+:::
+
+We use this approach to have access to StorefrontUI helpers in all components:
+
+```javascript
+// nuxt.config.js
+
+export default {
+ styleResources: {
+ scss: [
+ require.resolve('@storefront-ui/shared/styles/_helpers.scss', { paths: [process.cwd()] })
+ ]
+ },
+};
+```
diff --git a/docs/index.md b/docs/index.md
index 13dd1e2ce..3ca31ebe8 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -9,13 +9,13 @@ Welcome to the documentation of Vue Storefront 2 integration for Magento 2.
It's part of our ecosystem of integrations and extensions, which you can learn more about in our [core documentation](https://docs.vuestorefront.io/v2/).
-## Demo
-
-If you want to see the integration in action, check out our [demo environments](/guide/environments.html).
-
## Where to start?
To get started, see the following guides:
-- [Configuring Magento](/getting-started/configure-magento.html) to install the Magento server and configure it for Vue Storefront.
-- [Configuring Vue Storefront](/getting-started/configure-integration.html) to install Vue Storefront and configure it.
+- [Introduction](/getting-started/introduction.html) to learn what is Vue Storefront,
+- [Installation](/installation-setup/installation.html) to install and setup new Vue Storefront project.
+
+## Demo
+
+If you want to see the integration in action, check out our [demo environments](/guide/environments.html).
\ No newline at end of file
diff --git a/docs/getting-started/configure-integration.md b/docs/installation-setup/configure-integration.md
similarity index 94%
rename from docs/getting-started/configure-integration.md
rename to docs/installation-setup/configure-integration.md
index 89a911fc5..c2b93f003 100644
--- a/docs/getting-started/configure-integration.md
+++ b/docs/installation-setup/configure-integration.md
@@ -6,8 +6,8 @@ This guide explains the steps needed to set up Vue Storefront for Magento 2.
Before you can get started, you need:
-- Vue Storefront project created following the [Installation](/getting-started/installation.html) guide.
-- Magento 2 server configured following the [Configuring Magento](/getting-started/configure-magento.html) guide.
+- Vue Storefront project created following the [Installation](./installation.html) guide.
+- Magento 2 server configured following the [Configuring Magento](./configure-magento.html) guide.
## Creating the Vue Storefront for Magento 2
diff --git a/docs/getting-started/configure-magento.md b/docs/installation-setup/configure-magento.md
similarity index 100%
rename from docs/getting-started/configure-magento.md
rename to docs/installation-setup/configure-magento.md
diff --git a/docs/getting-started/installation.md b/docs/installation-setup/installation.md
similarity index 93%
rename from docs/getting-started/installation.md
rename to docs/installation-setup/installation.md
index e39aff446..d1cbe36ec 100644
--- a/docs/getting-started/installation.md
+++ b/docs/installation-setup/installation.md
@@ -38,11 +38,11 @@ npm install
### Step 2. Setup and configure Magento
-Before you can configure the project, you need to set up and configure a new Magento instance that Vue Storefront will connect to. To do so, follow the [Configuring Magento](/getting-started/configure-magento.html) guide.
+Before you can configure the project, you need to set up and configure a new Magento instance that Vue Storefront will connect to. To do so, follow the [Configuring Magento](./configure-magento.html) guide.
### Step 3. Configure Vue Storefront
-With the Magento instance setup and configured, you can connect your project to it. To do so, follow the [Configuring Vue Storefront](/getting-started/configure-integration.html) guide.
+With the Magento instance setup and configured, you can connect your project to it. To do so, follow the [Configuring Vue Storefront](./configure-integration.html) guide.
### Step 4. Start the project
diff --git a/docs/package.json b/docs/package.json
index 28242229e..776ac02e6 100755
--- a/docs/package.json
+++ b/docs/package.json
@@ -21,6 +21,7 @@
"@vuepress/plugin-medium-zoom": "^1.9.7",
"@vuepress/plugin-search": "^1.9.7",
"handlebars": "^4.7.7",
+ "markdown-it-video": "^0.6.3",
"rimraf": "^3.0.2",
"typescript": "^4.5.4",
"vuepress": "^1.9.7"
diff --git a/docs/yarn.lock b/docs/yarn.lock
index 07f6037fc..23a06de7c 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -5452,6 +5452,11 @@ markdown-it-table-of-contents@^0.4.0:
resolved "https://registry.yarnpkg.com/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz#3dc7ce8b8fc17e5981c77cc398d1782319f37fbc"
integrity sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==
+markdown-it-video@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/markdown-it-video/-/markdown-it-video-0.6.3.tgz#ee96a8fce4a262872f8aaf9706c31540d40104c2"
+ integrity sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==
+
markdown-it@^8.4.1:
version "8.4.2"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
diff --git a/internals/eslint-import/package.json b/internals/eslint-import/package.json
index 55c9a4997..d6b7ee740 100644
--- a/internals/eslint-import/package.json
+++ b/internals/eslint-import/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue-storefront/eslint-config-import",
- "version": "1.0.0-rc.9",
+ "version": "1.0.0-rc.10",
"author": "Heitor Ramon Ribeiro ",
"license": "MIT",
"scripts": {
diff --git a/internals/eslint-jest/package.json b/internals/eslint-jest/package.json
index 347a386f7..0a35792ae 100644
--- a/internals/eslint-jest/package.json
+++ b/internals/eslint-jest/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue-storefront/eslint-config-jest",
- "version": "1.0.0-rc.9",
+ "version": "1.0.0-rc.10",
"author": "Heitor Ramon Ribeiro ",
"license": "MIT",
"scripts": {
diff --git a/internals/eslint-typescript/package.json b/internals/eslint-typescript/package.json
index 017e17fb7..ae51967da 100644
--- a/internals/eslint-typescript/package.json
+++ b/internals/eslint-typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue-storefront/eslint-config-typescript",
- "version": "1.0.0-rc.9",
+ "version": "1.0.0-rc.10",
"author": "Heitor Ramon Ribeiro ",
"license": "MIT",
"scripts": {
diff --git a/internals/eslint-vue/package.json b/internals/eslint-vue/package.json
index 3c6f321f7..2a78fc560 100644
--- a/internals/eslint-vue/package.json
+++ b/internals/eslint-vue/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue-storefront/eslint-config-vue",
- "version": "1.0.0-rc.9",
+ "version": "1.0.0-rc.10",
"author": "Heitor Ramon Ribeiro ",
"license": "MIT",
"scripts": {
diff --git a/internals/eslint/package.json b/internals/eslint/package.json
index 8f8a3f2b2..b90f94e87 100644
--- a/internals/eslint/package.json
+++ b/internals/eslint/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue-storefront/eslint-config-base",
- "version": "1.0.0-rc.9",
+ "version": "1.0.0-rc.10",
"author": "Heitor Ramon Ribeiro ",
"license": "MIT",
"scripts": {
diff --git a/packages/api-client/package.json b/packages/api-client/package.json
index 1a071eed6..767787771 100644
--- a/packages/api-client/package.json
+++ b/packages/api-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue-storefront/magento-api",
- "version": "1.0.0-rc.9",
+ "version": "1.0.0-rc.10",
"sideEffects": false,
"homepage": "https://github.com/vuestorefront/magento2",
"bugs": {
diff --git a/packages/api-client/src/api/addProductsToCart/addProductsToCart.ts b/packages/api-client/src/api/addProductsToCart/addProductsToCart.ts
index 00b4f46ba..60e2d1c24 100644
--- a/packages/api-client/src/api/addProductsToCart/addProductsToCart.ts
+++ b/packages/api-client/src/api/addProductsToCart/addProductsToCart.ts
@@ -185,6 +185,10 @@ export default gql`
}
}
}
+ user_errors {
+ code
+ message
+ }
}
}
`;
diff --git a/packages/api-client/src/api/productDetail/productDetailsQuery.ts b/packages/api-client/src/api/productDetail/productDetailsQuery.ts
index f4e0b60bb..64c837a58 100644
--- a/packages/api-client/src/api/productDetail/productDetailsQuery.ts
+++ b/packages/api-client/src/api/productDetail/productDetailsQuery.ts
@@ -11,7 +11,6 @@ export default gql`
$pageSize: Int = 10,
$currentPage: Int = 1,
$sort: ProductAttributeSortInput
- $configurations: [ID!]
) {
products(search: $search, filter: $filter, sort: $sort, pageSize: $pageSize, currentPage: $currentPage) {
items {
@@ -30,28 +29,6 @@ export default gql`
url_rewrites {
url
}
- price_range {
- maximum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- minimum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- }
categories {
uid
name
@@ -73,28 +50,6 @@ export default gql`
}
}
}
- price_range {
- maximum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- minimum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- }
small_image {
url
position
@@ -134,50 +89,6 @@ export default gql`
}
options_container
special_to_date
- ... on BundleProduct {
- items {
- position
- required
- sku
- title
- type
- uid
- options {
- can_change_quantity
- is_default
- position
- uid
- quantity
- product {
- uid
- sku
- name
- price_range {
- maximum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- minimum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- }
- }
- }
- }
- }
... on ConfigurableProduct {
configurable_options {
attribute_code
@@ -194,87 +105,8 @@ export default gql`
uid
}
}
- configurable_product_options_selection(configurableOptionValueUids: $configurations) {
- options_available_for_selection {
- attribute_code
- option_value_uids
- }
- media_gallery {
- disabled
- label
- position
- url
- }
- variant {
- uid
- sku
- name
- price_range {
- maximum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- minimum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- }
- }
- }
- }
- ... on GroupedProduct {
- items {
- position
- qty
- product {
- uid
- sku
- name
- stock_status
- only_x_left_in_stock
- price_range {
- maximum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- minimum_price {
- final_price {
- currency
- value
- }
- regular_price {
- currency
- value
- }
- }
- }
- thumbnail {
- url
- position
- disabled
- label
- }
- }
- }
}
+
... on DownloadableProduct {
downloadable_product_samples {
sample_url
diff --git a/packages/api-client/src/types/API.ts b/packages/api-client/src/types/API.ts
index debc3e52d..0149170bf 100644
--- a/packages/api-client/src/types/API.ts
+++ b/packages/api-client/src/types/API.ts
@@ -3,13 +3,9 @@ import { ExecutionResult } from 'graphql';
import { CustomQuery } from '@vue-storefront/core';
import {
AddConfigurableProductsToCartInput,
- AddConfigurableProductsToCartMutation,
AddSimpleProductsToCartInput,
- AddSimpleProductsToCartMutation,
AddDownloadableProductsToCartInput,
- AddDownloadableProductsToCartMutation,
AddVirtualProductsToCartInput,
- AddVirtualProductsToCartMutation,
AppliedCoupon,
ApplyCouponToCartInput,
ApplyCouponToCartMutation,
@@ -95,11 +91,9 @@ import {
RemoveProductsFromWishlistMutationVariables,
RemoveProductsFromWishlistMutation,
GetCustomerAddressesQuery,
- AddProductsToCartMutation,
CmsBlockQuery,
GroupedProduct,
AddBundleProductsToCartInput,
- AddBundleProductsToCartMutation,
RequestPasswordResetEmailMutation,
RequestPasswordResetEmailMutationVariables,
ResetPasswordMutationVariables,
@@ -107,7 +101,12 @@ import {
ChangeCustomerPasswordMutation,
CreateCustomerAddressMutation,
DownloadableProduct,
- VirtualProduct, CustomerOrdersFilterInput, RoutableInterface,
+ VirtualProduct,
+ CustomerOrdersFilterInput,
+ RoutableInterface,
+ AddProductsToCartOutput,
+ AddConfigurableProductsToCartMutation,
+ AddBundleProductsToCartMutation, AddSimpleProductsToCartMutation, AddDownloadableProductsToCartMutation, AddVirtualProductsToCartMutation,
} from './GraphQL';
import { SetPaymentMethodOnCartInputs } from '../api/setPaymentMethodOnCart';
import { CustomerProductReviewParams } from '../api/customerProductReview';
@@ -179,7 +178,7 @@ export interface MagentoApiMethods {
addProductsToCart(
input: AddProductsToCartInput,
customQuery?: CustomQuery
- ): Promise>;
+ ): Promise>;
addProductToWishList(
input: AddProductsToWishlistMutationVariables,
diff --git a/packages/composables/package.json b/packages/composables/package.json
index 1939ac0ad..76c6a8d42 100644
--- a/packages/composables/package.json
+++ b/packages/composables/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue-storefront/magento",
- "version": "1.0.0-rc.9",
+ "version": "1.0.0-rc.10",
"license": "MIT",
"homepage": "https://github.com/vuestorefront/magento2",
"bugs": {
@@ -29,7 +29,7 @@
},
"dependencies": {
"@vue-storefront/core": "~2.5.6",
- "@vue-storefront/magento-api": "^1.0.0-rc.9",
+ "@vue-storefront/magento-api": "^1.0.0-rc.10",
"@vue/composition-api": "^1.4.1",
"cookie-universal": "^2.1.5",
"vue": "^2.6.14",
diff --git a/packages/theme/components/AppHeader.vue b/packages/theme/components/AppHeader.vue
index 046bec65e..ce5ac04ea 100644
--- a/packages/theme/components/AppHeader.vue
+++ b/packages/theme/components/AppHeader.vue
@@ -12,8 +12,14 @@
@@ -96,6 +102,7 @@
v-if="isSearchOpen"
:visible="isSearchOpen"
:search-results="productSearchResults"
+ @close="isSearchOpen = false"
/>
@@ -126,22 +133,21 @@ import { useWishlist } from '~/modules/wishlist/composables/useWishlist';
import { useUser } from '~/modules/customer/composables/useUser';
import { useWishlistStore } from '~/modules/wishlist/store/wishlistStore';
import type { CategoryTree, ProductInterface } from '~/modules/GraphQL/types';
-import CurrencySelector from '~/components/CurrencySelector.vue';
import HeaderLogo from '~/components/HeaderLogo.vue';
import SvgImage from '~/components/General/SvgImage.vue';
-import StoreSwitcher from '~/components/StoreSwitcher.vue';
+import { useTopBar } from './TopBar/useTopBar';
export default defineComponent({
components: {
HeaderNavigation,
SfHeader,
SfOverlay,
- CurrencySelector,
HeaderLogo,
- StoreSwitcher,
SvgImage,
SfButton,
SfBadge,
+ CurrencySelector: () => import('~/components/CurrencySelector.vue'),
+ StoreSwitcher: () => import('~/components/StoreSwitcher.vue'),
SearchBar: () => import('~/components/Header/SearchBar/SearchBar.vue'),
SearchResults: () => import(
/* webpackPrefetch: true */ '~/components/Header/SearchBar/SearchResults.vue'
@@ -157,6 +163,8 @@ export default defineComponent({
const { loadItemsCount: loadWishlistItemsCount } = useWishlist();
const { categories: categoryList, load: categoriesListLoad } = useCategory();
+ const { hasCurrencyToSelect, hasStoresToSelect } = useTopBar();
+
const isSearchOpen = ref(false);
const productSearchResults = ref(null);
@@ -204,6 +212,8 @@ export default defineComponent({
toggleWishlistSidebar,
wishlistHasProducts,
wishlistItemsQty,
+ hasCurrencyToSelect,
+ hasStoresToSelect,
};
},
});
diff --git a/packages/theme/components/BottomNavigation.vue b/packages/theme/components/BottomNavigation.vue
index 2c389fdd9..32222648d 100644
--- a/packages/theme/components/BottomNavigation.vue
+++ b/packages/theme/components/BottomNavigation.vue
@@ -4,7 +4,8 @@
@@ -31,6 +33,7 @@
@@ -44,6 +47,7 @@
@@ -56,11 +60,12 @@
-
+
import { SfBottomNavigation, SfCircleIcon } from '@storefront-ui/vue';
import { defineComponent, useRouter, useContext } from '@nuxtjs/composition-api';
-import { useUiState } from '~/composables';
+import { useUiState } from '~/composables/useUiState';
import { useUser } from '~/modules/customer/composables/useUser';
import SvgImage from '~/components/General/SvgImage.vue';
import { useCategoryStore } from '~/modules/catalog/category/stores/category';
@@ -86,6 +91,7 @@ import { useCategoryStore } from '~/modules/catalog/category/stores/category';
const MobileCategorySidebar = () => import('~/modules/catalog/category/components/sidebar/MobileCategorySidebar/MobileCategorySidebar.vue');
export default defineComponent({
+ name: 'BottomNavigation',
components: {
SfBottomNavigation,
SfCircleIcon,
@@ -103,6 +109,15 @@ export default defineComponent({
const { isAuthenticated } = useUser();
const router = useRouter();
const { app } = useContext();
+
+ const handleHomeClick = async () => {
+ const homePath = app.localeRoute({ name: 'home' });
+ await router.push(homePath);
+ if (isMobileMenuOpen.value) {
+ toggleMobileMenu();
+ }
+ };
+
const handleAccountClick = async () => {
if (isAuthenticated.value) {
await router.push(app.localeRoute({ name: 'customer' }));
@@ -127,7 +142,7 @@ export default defineComponent({
toggleMobileMenu,
loadCategoryMenu,
handleAccountClick,
- app,
+ handleHomeClick,
};
},
});
diff --git a/packages/theme/components/CartSidebar.vue b/packages/theme/components/CartSidebar.vue
index d8d333b4a..1c9e0eac3 100644
--- a/packages/theme/components/CartSidebar.vue
+++ b/packages/theme/components/CartSidebar.vue
@@ -92,6 +92,7 @@
updateItemQty(params),
1000,
);
@@ -548,31 +549,12 @@ export default defineComponent({
}
}
- &__actions {
- transition: opacity 150ms ease-in-out;
- }
-
- &__save,
- &__compare {
- --button-padding: 0;
-
- &:focus {
- --cp-save-opacity: 1;
- --cp-compare-opacity: 1;
- }
- }
-
- &__save {
- opacity: var(--cp-save-opacity, 0);
- }
-
- &__compare {
- opacity: var(--cp-compare-opacity, 0);
+ ::v-deep .sf-collected-product__actions {
+ display: none;
}
&:hover {
- --cp-save-opacity: 1;
- --cp-compare-opacity: 1;
+ --collected-product-configuration-display: initial;
@include for-desktop {
.collected-product__properties {
display: none;
diff --git a/packages/theme/components/Checkout/UserBillingAddresses.vue b/packages/theme/components/Checkout/UserBillingAddresses.vue
deleted file mode 100644
index 8321b8767..000000000
--- a/packages/theme/components/Checkout/UserBillingAddresses.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/theme/components/CurrencySelector/CurrenciesModal.vue b/packages/theme/components/CurrencySelector/CurrenciesModal.vue
index 8f2ba806c..5df7fdcbf 100644
--- a/packages/theme/components/CurrencySelector/CurrenciesModal.vue
+++ b/packages/theme/components/CurrencySelector/CurrenciesModal.vue
@@ -48,7 +48,10 @@ export default defineComponent({
},
props: {
isModalOpen: Boolean,
- selectedCurrency: String,
+ selectedCurrency: {
+ type: String,
+ default: '',
+ },
},
emits: ['closeModal'],
setup() {
@@ -58,7 +61,7 @@ export default defineComponent({
load: loadCurrencies,
} = useCurrency();
- const availableCurrencies = computed(() => currencies.value?.available_currency_codes || []);
+ const availableCurrencies = computed(() => currencies.value?.available_currency_codes || []);
onMounted(() => {
if (currencies.value && currencies.value?.available_currency_codes) return;
diff --git a/packages/theme/components/CurrencySelector/__tests__/CurrencySelector.spec.ts b/packages/theme/components/CurrencySelector/__tests__/CurrencySelector.spec.ts
new file mode 100644
index 000000000..72484a9d7
--- /dev/null
+++ b/packages/theme/components/CurrencySelector/__tests__/CurrencySelector.spec.ts
@@ -0,0 +1,80 @@
+import userEvent from '@testing-library/user-event';
+import { render } from '~/test-utils';
+import CurrenciesModal from '~/components/CurrencySelector/CurrenciesModal.vue';
+import { useCurrencyMock } from '~/test-utils/mocks/useCurrency';
+import { useCurrency } from '~/composables';
+import { currencyDataMock } from '~/components/CurrencySelector/__tests__/currencyData.mock';
+
+jest.mock('~/composables', () => {
+ const originalModule = jest.requireActual('~/composables');
+ return {
+ ...originalModule,
+ useCurrency: jest.fn(),
+ };
+});
+
+describe('', () => {
+ it('does not render when closed', () => {
+ (useCurrency as jest.Mock).mockReturnValue(useCurrencyMock);
+ const { queryByRole } = render(CurrenciesModal, {
+ props: {
+ isModalOpen: false,
+ },
+ });
+
+ expect(queryByRole('heading', { name: /change store/i })).toBeNull();
+ });
+
+ it('emits "closeModal" event on close', async () => {
+ const user = userEvent.setup();
+
+ (useCurrency as jest.Mock).mockReturnValue(useCurrencyMock);
+ const { getAllByRole, emitted } = render(CurrenciesModal, {
+ props: {
+ isModalOpen: true,
+ },
+ });
+
+ const closeBtn = getAllByRole('button', { name: /close/i })[0];
+
+ await user.click(closeBtn);
+
+ expect(emitted()).toHaveProperty('closeModal');
+ });
+
+ it('renders list of available stores', () => {
+ useCurrencyMock.currency.value = currencyDataMock;
+
+ (useCurrency as jest.Mock).mockReturnValue(useCurrencyMock);
+
+ const { getByRole, getByText } = render(CurrenciesModal, {
+ props: {
+ isModalOpen: true,
+ },
+ });
+
+ expect(getByRole('heading', { name: /choose currency/i })).toBeTruthy();
+ expect(getByText(/eur/i)).toBeTruthy();
+ expect(getByText(/pln/i)).toBeTruthy();
+ expect(getByText(/usd/i)).toBeTruthy();
+ });
+
+ it('on currency selection executes method that must trigger currency switch', async () => {
+ const user = userEvent.setup();
+ useCurrencyMock.currency.value = currencyDataMock;
+
+ (useCurrency as jest.Mock).mockReturnValue(useCurrencyMock);
+
+ const { getByText } = render(CurrenciesModal, {
+ props: {
+ isLangModalOpen: true,
+ storeConfig: { store_code: 'default' },
+ },
+ });
+
+ const eurSwitchBtn = getByText(/eur/i);
+ await user.click(eurSwitchBtn);
+
+ expect(useCurrencyMock.change).toBeCalledWith({ id: 'EUR' });
+ });
+});
diff --git a/packages/theme/components/CurrencySelector/__tests__/currencyData.mock.ts b/packages/theme/components/CurrencySelector/__tests__/currencyData.mock.ts
new file mode 100644
index 000000000..dfe0305a7
--- /dev/null
+++ b/packages/theme/components/CurrencySelector/__tests__/currencyData.mock.ts
@@ -0,0 +1,31 @@
+export const currencyDataMock = {
+ __typename: 'Currency',
+ available_currency_codes: [
+ 'EUR',
+ 'PLN',
+ 'USD',
+ ],
+ base_currency_code: 'USD',
+ base_currency_symbol: '$',
+ default_display_currecy_code: null,
+ default_display_currecy_symbol: null,
+ default_display_currency_code: 'USD',
+ default_display_currency_symbol: '$',
+ exchange_rates: [
+ {
+ __typename: 'ExchangeRate',
+ currency_to: 'EUR',
+ rate: 0.93,
+ },
+ {
+ __typename: 'ExchangeRate',
+ currency_to: 'PLN',
+ rate: 4.29,
+ },
+ {
+ __typename: 'ExchangeRate',
+ currency_to: 'USD',
+ rate: 1,
+ },
+ ],
+};
diff --git a/packages/theme/components/Header/Navigation/HeaderNavigationSubcategories.vue b/packages/theme/components/Header/Navigation/HeaderNavigationSubcategories.vue
index 490f8ff3f..b83802ca6 100644
--- a/packages/theme/components/Header/Navigation/HeaderNavigationSubcategories.vue
+++ b/packages/theme/components/Header/Navigation/HeaderNavigationSubcategories.vue
@@ -181,7 +181,7 @@ export default defineComponent({
.header-navigation {
&__subcategories {
position: absolute;
- z-index: 10;
+ z-index: 1;
background-color: #fff;
box-shadow: 0 3px var(--c-primary);
left: 0;
diff --git a/packages/theme/components/Header/SearchBar/SearchBar.vue b/packages/theme/components/Header/SearchBar/SearchBar.vue
index efb3b0603..df2a5d370 100644
--- a/packages/theme/components/Header/SearchBar/SearchBar.vue
+++ b/packages/theme/components/Header/SearchBar/SearchBar.vue
@@ -5,8 +5,8 @@
aria-label="Search"
class="sf-header__search"
:value="term"
- @input="handleSearch"
- @keydown.enter="handleSearch($event)"
+ @input="debouncedHandleSearch($event)"
+ @keyup.enter="handleKeydownEnter($event.target.value) /* https://github.com/vuestorefront/storefront-ui/issues/2453#issuecomment-1160231619 */"
@keydown.tab="hideSearch"
@focus="showSearch"
@click="showSearch"
@@ -53,7 +53,7 @@ import { SfButton, SfSearchBar } from '@storefront-ui/vue';
import {
defineComponent, ref, watch, useRoute,
} from '@nuxtjs/composition-api';
-import debounce from 'lodash.debounce';
+import { debounce } from 'lodash-es';
import { clickOutside } from '~/utilities/directives/click-outside/click-outside-directive';
import SvgImage from '~/components/General/SvgImage.vue';
import { useProduct } from '~/modules/catalog/product/composables/useProduct';
@@ -130,7 +130,7 @@ export default defineComponent({
}
};
- const handleSearch = debounce(async (searchTerm: string) => {
+ const rawHandleSearch = async (searchTerm: string) => {
term.value = searchTerm;
if (term.value.length < props.minTermLen) return;
@@ -141,7 +141,15 @@ export default defineComponent({
}) as unknown as Products;
emit('set-search-results', productList!.items);
- }, 1000);
+ };
+
+ const debouncedHandleSearch = debounce(rawHandleSearch, 1000);
+
+ const handleKeydownEnter = (searchTerm: string) => {
+ // cancel debounce timeout started by typing into the searchbar - this is to avoid making two network requests instead of one
+ debouncedHandleSearch.cancel();
+ rawHandleSearch(searchTerm);
+ };
watch(route, () => {
hideSearch();
@@ -153,7 +161,9 @@ export default defineComponent({
showSearch,
hideSearch,
toggleSearch,
- handleSearch,
+ rawHandleSearch,
+ debouncedHandleSearch,
+ handleKeydownEnter,
term,
};
},
diff --git a/packages/theme/components/Header/SearchBar/SearchResults.vue b/packages/theme/components/Header/SearchBar/SearchResults.vue
index c2294582f..4fe179448 100644
--- a/packages/theme/components/Header/SearchBar/SearchResults.vue
+++ b/packages/theme/components/Header/SearchBar/SearchResults.vue
@@ -202,6 +202,7 @@ export default defineComponent({
flex-direction: row;
flex: 1;
}
+ width: 100%;
}
&__results {
flex: 1;
@@ -226,7 +227,7 @@ export default defineComponent({
&--mobile {
display: flex;
flex-wrap: wrap;
- justify-content: space-around;
+ justify-content: flex-start;
background: var(--c-white);
padding: var(--spacer-base) var(--spacer-sm);
--product-card-max-width: 9rem;
diff --git a/packages/theme/components/MobileStoreBanner.vue b/packages/theme/components/MobileStoreBanner.vue
index c7175ff0e..da552a17b 100644
--- a/packages/theme/components/MobileStoreBanner.vue
+++ b/packages/theme/components/MobileStoreBanner.vue
@@ -13,13 +13,15 @@
data-testid="banner-cta-button"
link="#"
>
-
+
+
+
-
+
+
+
@@ -92,4 +96,9 @@ export default defineComponent({
}
}
}
+// override ".sf-banner img" style spill
+::v-deep .app-store-image img {
+ position: initial;
+ margin-top: var(--spacer-base);
+}
diff --git a/packages/theme/components/SkeletonLoader/index.vue b/packages/theme/components/SkeletonLoader/index.vue
index 2c7357f0e..9992e2244 100644
--- a/packages/theme/components/SkeletonLoader/index.vue
+++ b/packages/theme/components/SkeletonLoader/index.vue
@@ -9,7 +9,7 @@
diff --git a/packages/theme/modules/catalog/product/components/product-types/bundle/BundleProductSelector.vue b/packages/theme/modules/catalog/product/components/product-types/bundle/BundleProductSelector.vue
index 1af5ef4eb..dee0e8aa2 100644
--- a/packages/theme/modules/catalog/product/components/product-types/bundle/BundleProductSelector.vue
+++ b/packages/theme/modules/catalog/product/components/product-types/bundle/BundleProductSelector.vue
@@ -73,6 +73,12 @@
>
Add to Cart
+
+
+
diff --git a/packages/theme/components/Checkout/UserShippingAddresses.vue b/packages/theme/modules/checkout/components/UserShippingAddresses.vue
similarity index 53%
rename from packages/theme/components/Checkout/UserShippingAddresses.vue
rename to packages/theme/modules/checkout/components/UserShippingAddresses.vue
index b47c921ce..0a25420eb 100644
--- a/packages/theme/components/Checkout/UserShippingAddresses.vue
+++ b/packages/theme/modules/checkout/components/UserShippingAddresses.vue
@@ -2,16 +2,20 @@
-
+
+
+ {{ shippingAddress.countryName }}
+
+
@@ -28,12 +32,13 @@
diff --git a/packages/theme/components/Checkout/VsfPaymentProvider.vue b/packages/theme/modules/checkout/components/VsfPaymentProvider.vue
similarity index 100%
rename from packages/theme/components/Checkout/VsfPaymentProvider.vue
rename to packages/theme/modules/checkout/components/VsfPaymentProvider.vue
diff --git a/packages/theme/components/Checkout/VsfShippingProvider.vue b/packages/theme/modules/checkout/components/VsfShippingProvider.vue
similarity index 100%
rename from packages/theme/components/Checkout/VsfShippingProvider.vue
rename to packages/theme/modules/checkout/components/VsfShippingProvider.vue
diff --git a/packages/theme/modules/checkout/components/styles/userAddresses.scss b/packages/theme/modules/checkout/components/styles/userAddresses.scss
new file mode 100644
index 000000000..12ee42a7a
--- /dev/null
+++ b/packages/theme/modules/checkout/components/styles/userAddresses.scss
@@ -0,0 +1,27 @@
+.address {
+ margin-bottom: var(--spacer-base);
+ @include for-desktop {
+ margin-right: var(--spacer-sm);
+ display: flex;
+ width: calc(50% - var(--spacer-sm));
+ flex-direction: column;
+ }
+}
+
+.addresses {
+ margin-bottom: var(--spacer-xl);
+ @include for-desktop {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: var(--spacer-sm)
+ }
+}
+
+.setAsDefault {
+ margin-bottom: var(--spacer-xl);
+}
+
+.sf-divider,
+.form__action-button--margin-bottom {
+ margin-bottom: var(--spacer-xl);
+}
diff --git a/packages/theme/modules/checkout/composables/useBilling/commands/saveBillingAddressCommand.ts b/packages/theme/modules/checkout/composables/useBilling/commands/saveBillingAddressCommand.ts
index 526125891..f536fc19f 100644
--- a/packages/theme/modules/checkout/composables/useBilling/commands/saveBillingAddressCommand.ts
+++ b/packages/theme/modules/checkout/composables/useBilling/commands/saveBillingAddressCommand.ts
@@ -2,7 +2,7 @@ import { Logger } from '~/helpers/logger';
import { BillingCartAddress, Maybe, SetBillingAddressOnCartInput } from '~/modules/GraphQL/types';
export const saveBillingAddressCommand = {
- execute: async (context, cartId, billingDetails): Promise
> => {
+ execute: async (context, cartId, billingDetails, customQuery): Promise> => {
const {
apartment,
neighborhood,
@@ -29,6 +29,7 @@ export const saveBillingAddressCommand = {
const { data } = await context.$vsf.$magento.api.setBillingAddressOnCart(
setBillingAddressOnCartInput,
+ customQuery,
);
Logger.debug('[Result]:', { data });
diff --git a/packages/theme/modules/checkout/composables/useBilling/index.ts b/packages/theme/modules/checkout/composables/useBilling/index.ts
index fd6985138..a1d8a0e04 100644
--- a/packages/theme/modules/checkout/composables/useBilling/index.ts
+++ b/packages/theme/modules/checkout/composables/useBilling/index.ts
@@ -50,13 +50,13 @@ export function useBilling(): UseBillingInterface {
return billingInfo;
};
- const save = async ({ billingDetails }: UseBillingSaveParams): Promise> => {
+ const save = async ({ billingDetails, customQuery = null }: UseBillingSaveParams): Promise> => {
Logger.debug('useBilling.save');
let billingInfo = null;
try {
loading.value = true;
- billingInfo = await saveBillingAddressCommand.execute(context, cart.value.id, billingDetails);
+ billingInfo = await saveBillingAddressCommand.execute(context, cart.value.id, billingDetails, customQuery);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
/**
diff --git a/packages/theme/modules/checkout/composables/useBilling/useBilling.ts b/packages/theme/modules/checkout/composables/useBilling/useBilling.ts
index 2fed2597a..d17253b31 100644
--- a/packages/theme/modules/checkout/composables/useBilling/useBilling.ts
+++ b/packages/theme/modules/checkout/composables/useBilling/useBilling.ts
@@ -44,9 +44,9 @@ export type UseBillingLoadParams = ComposableFunctionArgs<{}>;
/**
* The params object accepted by the `save` method in the {@link useBilling|useBilling()} composable
*/
-export interface UseBillingSaveParams {
+export type UseBillingSaveParams = ComposableFunctionArgs<{
billingDetails: BillingDetails;
-}
+}>;
/**
* Data and methods returned from the {@link useBilling|useBilling()} composable
diff --git a/packages/theme/modules/checkout/composables/useCart/commands/addItemCommand.ts b/packages/theme/modules/checkout/composables/useCart/commands/addItemCommand.ts
index 9c693288f..92e0c1d41 100644
--- a/packages/theme/modules/checkout/composables/useCart/commands/addItemCommand.ts
+++ b/packages/theme/modules/checkout/composables/useCart/commands/addItemCommand.ts
@@ -1,12 +1,10 @@
import { VsfContext } from '~/composables/context';
import { Logger } from '~/helpers/logger';
import {
- AddConfigurableProductsToCartInput,
- AddDownloadableProductsToCartInput,
- AddVirtualProductsToCartInput,
Cart,
CartItemInput,
} from '~/modules/GraphQL/types';
+import { CustomQuery } from '~/types/core';
/** Params object used to add items to a cart */
export declare type AddProductsToCartInput = {
@@ -21,6 +19,8 @@ export const addItemCommand = {
product,
quantity,
currentCart,
+ productConfiguration,
+ customQuery,
},
) => {
Logger.debug('[Magento]: Add item to cart', {
@@ -30,7 +30,7 @@ export const addItemCommand = {
});
const apiState = context.$magento.config.state;
- const currentCartId = apiState.getCartId();
+ const cartId = apiState.getCartId();
if (!product) {
return;
@@ -39,7 +39,7 @@ export const addItemCommand = {
switch (product.__typename) {
case 'SimpleProduct':
const simpleCartInput: AddProductsToCartInput = {
- cartId: currentCartId,
+ cartId,
cartItems: [
{
quantity,
@@ -48,9 +48,13 @@ export const addItemCommand = {
],
};
- const simpleProduct = await context.$magento.api.addProductsToCart(simpleCartInput);
+ const simpleProduct = await context.$magento.api.addProductsToCart(simpleCartInput, customQuery as CustomQuery);
- Logger.debug('[Result]:', { data: simpleProduct });
+ Logger.debug('[Result]:', { data: simpleProduct.data });
+
+ if (simpleProduct.data.addProductsToCart.user_errors.length > 0) {
+ throw new Error(String(simpleProduct.data.addProductsToCart.user_errors[0].message));
+ }
// eslint-disable-next-line consistent-return
return simpleProduct
@@ -58,29 +62,29 @@ export const addItemCommand = {
.addProductsToCart
.cart as unknown as Cart;
case 'ConfigurableProduct':
- const cartItems = [
- {
- parent_sku: product.sku,
- data: {
+ const selectedOptions = Object.values(productConfiguration as object);
+
+ const configurableCartInput: AddProductsToCartInput = {
+ cartId,
+ cartItems: [
+ {
quantity,
- sku: product.configurable_product_options_selection?.variant?.sku || '',
+ sku: product.sku,
+ selected_options: selectedOptions,
},
- },
- ];
-
- const configurableCartInput: AddConfigurableProductsToCartInput = {
- cart_id: currentCartId,
- cart_items: cartItems,
+ ],
};
- const configurableProduct = await context.$magento.api.addConfigurableProductsToCart(configurableCartInput);
+ const configurableProduct = await context.$magento.api.addProductsToCart(configurableCartInput, customQuery as CustomQuery);
+ Logger.debug('[Result]:', { data: configurableProduct.data });
- Logger.debug('[Result]:', { data: configurableProduct });
+ if (configurableProduct.data.addProductsToCart.user_errors.length > 0) {
+ throw new Error(String(configurableProduct.data.addProductsToCart.user_errors[0].message));
+ }
// eslint-disable-next-line consistent-return
- return configurableProduct
- .data
- .addConfigurableProductsToCart
+ return configurableProduct.data
+ .addProductsToCart
.cart as unknown as Cart;
case 'BundleProduct':
const createEnteredOptions = () =>
@@ -91,7 +95,7 @@ export const addItemCommand = {
}));
const bundleCartInput: AddProductsToCartInput = {
- cartId: currentCartId,
+ cartId,
cartItems: [
{
quantity,
@@ -101,60 +105,65 @@ export const addItemCommand = {
],
};
- const bundleProduct = await context.$magento.api.addProductsToCart(bundleCartInput);
+ const bundleProduct = await context.$magento.api.addProductsToCart(bundleCartInput, customQuery as CustomQuery);
Logger.debug('[Result]:', { data: bundleProduct });
+ if (bundleProduct.data.addProductsToCart.user_errors.length > 0) {
+ throw new Error(String(bundleProduct.data.addProductsToCart.user_errors[0].message));
+ }
+
// eslint-disable-next-line consistent-return
return bundleProduct
.data
.addProductsToCart
.cart as unknown as Cart;
case 'DownloadableProduct':
- const downloadableCartItems = [
- {
- data: {
+ const downloadableCartInput: AddProductsToCartInput = {
+ cartId,
+ cartItems: [
+ {
quantity,
sku: product.sku,
},
- downloadable_product_links: product.downloadable_product_links.map((dpl) => ({ link_id: dpl.id })),
- },
- ];
-
- const downloadableCartInput: AddDownloadableProductsToCartInput = {
- cart_id: currentCartId,
- cart_items: downloadableCartItems,
+ ],
};
- const downloadableProduct = await context.$magento.api.addDownloadableProductsToCart(downloadableCartInput);
+ const downloadableProduct = await context.$magento.api.addProductsToCart(downloadableCartInput, customQuery as CustomQuery);
Logger.debug('[Result DownloadableProduct]:', { data: downloadableProduct });
+ if (downloadableProduct.data.addProductsToCart.user_errors.length > 0) {
+ throw new Error(String(downloadableProduct.data.addProductsToCart.user_errors[0].message));
+ }
+
// eslint-disable-next-line consistent-return
return downloadableProduct
.data
- .addDownloadableProductsToCart
+ .addProductsToCart
.cart as unknown as Cart;
case 'VirtualProduct':
- const virtualCartInput: AddVirtualProductsToCartInput = {
- cart_id: currentCartId,
- cart_items: [
+ const virtualCartInput: AddProductsToCartInput = {
+ cartId,
+ cartItems: [
{
- data: {
- quantity,
- sku: product.sku,
- },
+ quantity,
+ sku: product.sku,
},
],
};
- const virtualProduct = await context.$magento.api.addVirtualProductsToCart(virtualCartInput);
+ const virtualProduct = await context.$magento.api.addProductsToCart(virtualCartInput, customQuery as CustomQuery);
Logger.debug('[Result VirtualProduct]:', { data: virtualProduct });
+ if (downloadableProduct.data.addProductsToCart.user_errors.length > 0) {
+ throw new Error(String(downloadableProduct.data.addProductsToCart.user_errors[0].message));
+ }
+
// eslint-disable-next-line consistent-return
return virtualProduct
.data
- .addVirtualProductsToCart
+ .addProductsToCart
.cart as unknown as Cart;
default:
// eslint-disable-next-line no-underscore-dangle
diff --git a/packages/theme/modules/checkout/composables/useCart/commands/clearCartCommand.ts b/packages/theme/modules/checkout/composables/useCart/commands/clearCartCommand.ts
deleted file mode 100644
index 5360c27bf..000000000
--- a/packages/theme/modules/checkout/composables/useCart/commands/clearCartCommand.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { VsfContext } from '~/composables/context';
-
-export const clearCartCommand = {
- execute: (context: VsfContext) => {
- context.$magento.config.state.setCartId(null);
- },
-};
diff --git a/packages/theme/modules/checkout/composables/useCart/commands/loadTotalQtyCommand.ts b/packages/theme/modules/checkout/composables/useCart/commands/loadTotalQtyCommand.ts
index cb56872fa..2a02c90fa 100644
--- a/packages/theme/modules/checkout/composables/useCart/commands/loadTotalQtyCommand.ts
+++ b/packages/theme/modules/checkout/composables/useCart/commands/loadTotalQtyCommand.ts
@@ -1,13 +1,14 @@
import { Logger } from '~/helpers/logger';
import { VsfContext } from '~/composables/context';
+import { ComposableFunctionArgs } from '~/composables';
export const loadTotalQtyCommand = {
- execute: async (context: VsfContext) => {
+ execute: async (context: VsfContext, params?: ComposableFunctionArgs<{}>) => {
Logger.debug('[Magento]: Load cart total qty');
const apiState = context.$magento.config.state;
if (apiState.getCartId()) {
- const { data } : any = await context.$magento.api.cartTotalQty(apiState.getCartId());
+ const { data } : any = await context.$magento.api.cartTotalQty(apiState.getCartId(), params?.customQuery ?? null);
return data?.cart?.total_quantity ?? 0;
}
diff --git a/packages/theme/modules/checkout/composables/useCart/commands/removeItemCommand.ts b/packages/theme/modules/checkout/composables/useCart/commands/removeItemCommand.ts
index 380117635..5f56694b3 100644
--- a/packages/theme/modules/checkout/composables/useCart/commands/removeItemCommand.ts
+++ b/packages/theme/modules/checkout/composables/useCart/commands/removeItemCommand.ts
@@ -1,11 +1,12 @@
import { Logger } from '~/helpers/logger';
import { Cart, RemoveItemFromCartInput } from '~/modules/GraphQL/types';
import { VsfContext } from '~/composables/context';
+import { CustomQuery } from '~/types/core';
export const removeItemCommand = {
execute: async (
context: VsfContext,
- { currentCart, product },
+ { currentCart, product, customQuery },
) => {
Logger.debug('[Magento]: Remove item from cart', {
product,
@@ -23,7 +24,7 @@ export const removeItemCommand = {
cart_item_uid: item.uid,
};
- const { data } = await context.$magento.api.removeItemFromCart(removeItemParams);
+ const { data } = await context.$magento.api.removeItemFromCart(removeItemParams, customQuery as CustomQuery);
Logger.debug('[Result]:', { data });
diff --git a/packages/theme/modules/checkout/composables/useCart/index.ts b/packages/theme/modules/checkout/composables/useCart/index.ts
index e7c16ee6c..c573572ec 100644
--- a/packages/theme/modules/checkout/composables/useCart/index.ts
+++ b/packages/theme/modules/checkout/composables/useCart/index.ts
@@ -3,7 +3,6 @@ import {
} from '@nuxtjs/composition-api';
import { addItemCommand } from '~/modules/checkout/composables/useCart/commands/addItemCommand';
import { applyCouponCommand } from '~/modules/checkout/composables/useCart/commands/applyCouponCommand';
-import { clearCartCommand } from '~/modules/checkout/composables/useCart/commands/clearCartCommand';
import { loadCartCommand } from '~/modules/checkout/composables/useCart/commands/loadCartCommand';
import { loadTotalQtyCommand } from '~/modules/checkout/composables/useCart/commands/loadTotalQtyCommand';
import { removeCouponCommand } from '~/modules/checkout/composables/useCart/commands/removeCouponCommand';
@@ -13,8 +12,9 @@ import { Logger } from '~/helpers/logger';
import { Cart, CartItemInterface, ProductInterface } from '~/modules/GraphQL/types';
import { useCartStore } from '~/modules/checkout/stores/cart';
import { useWishlist } from '~/modules/wishlist/composables/useWishlist';
-import { UseCartErrors, UseCartInterface } from './useCart';
import { Product } from '~/modules/catalog/product/types';
+import { ComposableFunctionArgs } from '~/composables';
+import { UseCartErrors, UseCartInterface } from './useCart';
/**
* Allows loading and manipulating cart of the current user.
@@ -90,7 +90,7 @@ PRODUCT
try {
loading.value = true;
- clearCartCommand.execute(context);
+ context.$magento.config.state.removeCartId();
const loadedCart = await loadCartCommand.execute(context, { customQuery });
cartStore.$patch((state) => {
@@ -104,12 +104,12 @@ PRODUCT
}
};
- const loadTotalQty = async (): Promise => {
+ const loadTotalQty = async (params?: ComposableFunctionArgs<{}>): Promise => {
Logger.debug('useCart.loadTotalQty');
try {
loading.value = true;
- const totalQuantity = await loadTotalQtyCommand.execute(context);
+ const totalQuantity = await loadTotalQtyCommand.execute(context, params);
cartStore.$patch((state) => {
state.cart.total_quantity = totalQuantity;
@@ -122,7 +122,9 @@ PRODUCT
}
};
- const addItem = async ({ product, quantity }): Promise => {
+ const addItem = async ({
+ product, quantity, productConfiguration, customQuery,
+ }): Promise => {
Logger.debug('useCart.addItem', { product, quantity });
try {
@@ -136,7 +138,10 @@ PRODUCT
currentCart: cart.value,
product,
quantity,
+ productConfiguration,
+ customQuery,
});
+
error.value.addItem = null;
cartStore.$patch((state) => {
state.cart = updatedCart;
@@ -155,7 +160,7 @@ PRODUCT
}
};
- const removeItem = async ({ product }) => {
+ const removeItem = async ({ product, customQuery }) => {
Logger.debug('useCart.removeItem', { product });
try {
@@ -163,6 +168,7 @@ PRODUCT
const updatedCart = await removeItemCommand.execute(context, {
currentCart: cart.value,
product,
+ customQuery,
});
error.value.removeItem = null;
diff --git a/packages/theme/modules/checkout/composables/useCart/useCart.ts b/packages/theme/modules/checkout/composables/useCart/useCart.ts
index 96073ff91..8cd34e645 100644
--- a/packages/theme/modules/checkout/composables/useCart/useCart.ts
+++ b/packages/theme/modules/checkout/composables/useCart/useCart.ts
@@ -8,6 +8,7 @@ import { Product } from '~/modules/catalog/product/types';
export type UseCartAddItemParams = ComposableFunctionArgs<{
product: PRODUCT;
quantity: number;
+ productConfiguration?: { [key: string]: string };
}>;
/**
diff --git a/packages/theme/modules/checkout/composables/useGetShippingMethods/commands/getCustomerShippingMethodsCommand.ts b/packages/theme/modules/checkout/composables/useGetShippingMethods/commands/getCustomerShippingMethodsCommand.ts
index 30f32b24b..a2ffa5882 100644
--- a/packages/theme/modules/checkout/composables/useGetShippingMethods/commands/getCustomerShippingMethodsCommand.ts
+++ b/packages/theme/modules/checkout/composables/useGetShippingMethods/commands/getCustomerShippingMethodsCommand.ts
@@ -1,9 +1,12 @@
import { AvailableShippingMethod } from '~/modules/GraphQL/types';
import { VsfContext } from '~/composables/context';
+import { ComposableFunctionArgs } from '~/composables';
export const getCustomerShippingMethodsCommand = {
- execute: async (context: VsfContext): Promise => {
- const { data: { customerCart: { shipping_addresses: shippingAddresses } } } = await context.$magento.api.getAvailableCustomerShippingMethods();
+ execute: async (context: VsfContext, params: ComposableFunctionArgs<{}>): Promise => {
+ const {
+ data: { customerCart: { shipping_addresses: shippingAddresses } },
+ } = await context.$magento.api.getAvailableCustomerShippingMethods(params?.customQuery ?? null);
return shippingAddresses[0]?.available_shipping_methods ?? [];
},
};
diff --git a/packages/theme/modules/checkout/composables/useGetShippingMethods/commands/getGuestShippingMethodsCommand.ts b/packages/theme/modules/checkout/composables/useGetShippingMethods/commands/getGuestShippingMethodsCommand.ts
index 531158c38..2212e2b2b 100644
--- a/packages/theme/modules/checkout/composables/useGetShippingMethods/commands/getGuestShippingMethodsCommand.ts
+++ b/packages/theme/modules/checkout/composables/useGetShippingMethods/commands/getGuestShippingMethodsCommand.ts
@@ -4,7 +4,7 @@ import { AvailableShippingMethod } from '~/modules/GraphQL/types';
export const getGuestShippingMethodsCommand = {
execute: async (context: Context['app'], params: ComposableFunctionArgs<{ cartId: string }>): Promise => {
- const { data } = await context.$vsf.$magento.api.getAvailableShippingMethods({ cartId: params.cartId });
+ const { data } = await context.$vsf.$magento.api.getAvailableShippingMethods({ cartId: params.cartId }, params?.customQuery ?? null);
const hasAddresses = data.cart.shipping_addresses.length > 0;
return hasAddresses ? data?.cart?.shipping_addresses[0]?.available_shipping_methods : [];
diff --git a/packages/theme/modules/checkout/composables/useGetShippingMethods/index.ts b/packages/theme/modules/checkout/composables/useGetShippingMethods/index.ts
index aa6799183..545900c54 100644
--- a/packages/theme/modules/checkout/composables/useGetShippingMethods/index.ts
+++ b/packages/theme/modules/checkout/composables/useGetShippingMethods/index.ts
@@ -30,7 +30,7 @@ export function useGetShippingMethods(): UseGetShippingMethodsInterface => {
- const { data } = await context.app.$vsf.$magento.api.placeOrder({ cart_id: cartId });
+ execute: async (context: UseContextReturn, cartId: string, params?: ComposableFunctionArgs<{}>): Promise => {
+ const { data } = await context.app.$vsf.$magento.api.placeOrder({ cart_id: cartId }, params?.customQuery ?? null);
return data?.placeOrder ?? null;
},
diff --git a/packages/theme/modules/checkout/composables/useMakeOrder/index.ts b/packages/theme/modules/checkout/composables/useMakeOrder/index.ts
index 687cc0596..3ec8b7acf 100644
--- a/packages/theme/modules/checkout/composables/useMakeOrder/index.ts
+++ b/packages/theme/modules/checkout/composables/useMakeOrder/index.ts
@@ -4,6 +4,7 @@ import { placeOrderCommand } from '~/modules/checkout/composables/useMakeOrder/c
import useCart from '~/modules/checkout/composables/useCart';
import type { PlaceOrderOutput } from '~/modules/GraphQL/types';
import type { UseMakeOrderErrors, UseMakeOrderInterface } from './useMakeOrder';
+import { ComposableFunctionArgs } from '~/composables';
/**
* Allows making an order from a cart.
@@ -16,12 +17,12 @@ export function useMakeOrder(): UseMakeOrderInterface {
const { cart } = useCart();
const context = useContext();
- const make = async (): Promise => {
+ const make = async (params?: ComposableFunctionArgs<{}>): Promise => {
Logger.debug('useMakeOrder.make');
let placedOrder = null;
try {
loading.value = true;
- placedOrder = await placeOrderCommand.execute(context, cart.value.id);
+ placedOrder = await placeOrderCommand.execute(context, cart.value.id, params?.customQuery ?? null);
error.value.make = null;
} catch (err) {
error.value.make = err;
diff --git a/packages/theme/modules/checkout/composables/useMakeOrder/useMakeOrder.ts b/packages/theme/modules/checkout/composables/useMakeOrder/useMakeOrder.ts
index 32ff0ddd0..b2812fc79 100644
--- a/packages/theme/modules/checkout/composables/useMakeOrder/useMakeOrder.ts
+++ b/packages/theme/modules/checkout/composables/useMakeOrder/useMakeOrder.ts
@@ -1,5 +1,6 @@
import type { Ref } from '@nuxtjs/composition-api';
import type { PlaceOrderOutput } from '~/modules/GraphQL/types';
+import { ComposableFunctionArgs } from '~/composables';
/**
* The {@link useMakeOrder} error object. The properties values' are the errors
@@ -15,7 +16,7 @@ export interface UseMakeOrderErrors {
*/
export interface UseMakeOrderInterface {
/** Makes an order with current cart. */
- make(): Promise;
+ make(params?: ComposableFunctionArgs<{}>): Promise;
/**
* Contains errors from any of the composable methods.
diff --git a/packages/theme/modules/checkout/composables/usePaymentProvider/commands/getAvailablePaymentMethodsCommand.ts b/packages/theme/modules/checkout/composables/usePaymentProvider/commands/getAvailablePaymentMethodsCommand.ts
index db6714b37..fb46c4718 100644
--- a/packages/theme/modules/checkout/composables/usePaymentProvider/commands/getAvailablePaymentMethodsCommand.ts
+++ b/packages/theme/modules/checkout/composables/usePaymentProvider/commands/getAvailablePaymentMethodsCommand.ts
@@ -1,9 +1,9 @@
-import { UseContextReturn } from '~/types/core';
+import { CustomQuery, UseContextReturn } from '~/types/core';
import type { AvailablePaymentMethod } from '~/modules/GraphQL/types';
export const getAvailablePaymentMethodsCommand = {
- execute: async (context: UseContextReturn, cartId: string): Promise => {
- const { data } = await context.app.$vsf.$magento.api.getAvailablePaymentMethods({ cartId });
+ execute: async (context: UseContextReturn, cartId: string, customQuery?: CustomQuery): Promise => {
+ const { data } = await context.app.$vsf.$magento.api.getAvailablePaymentMethods({ cartId }, customQuery);
return data?.cart?.available_payment_methods ?? [];
},
diff --git a/packages/theme/modules/checkout/composables/usePaymentProvider/commands/setPaymentMethodOnCartCommand.ts b/packages/theme/modules/checkout/composables/usePaymentProvider/commands/setPaymentMethodOnCartCommand.ts
index 09fa90323..b3c0e45ec 100644
--- a/packages/theme/modules/checkout/composables/usePaymentProvider/commands/setPaymentMethodOnCartCommand.ts
+++ b/packages/theme/modules/checkout/composables/usePaymentProvider/commands/setPaymentMethodOnCartCommand.ts
@@ -4,7 +4,7 @@ import type { PaymentMethodParams } from '../usePaymentProvider';
export const setPaymentMethodOnCartCommand = {
execute: async (context: UseContextReturn, params: PaymentMethodParams): Promise => {
- const { data } = await context.app.$vsf.$magento.api.setPaymentMethodOnCart(params);
+ const { data } = await context.app.$vsf.$magento.api.setPaymentMethodOnCart(params, params?.customQuery ?? null);
return data?.setPaymentMethodOnCart?.cart.available_payment_methods ?? [];
},
diff --git a/packages/theme/modules/checkout/composables/usePaymentProvider/index.ts b/packages/theme/modules/checkout/composables/usePaymentProvider/index.ts
index f554dc82a..1e5a18478 100644
--- a/packages/theme/modules/checkout/composables/usePaymentProvider/index.ts
+++ b/packages/theme/modules/checkout/composables/usePaymentProvider/index.ts
@@ -10,6 +10,7 @@ import type {
UsePaymentProviderSaveParams,
PaymentMethodParams,
} from './usePaymentProvider';
+import { CustomQuery } from '~/types/core';
/**
* Allows loading the available payment
@@ -37,6 +38,7 @@ export function usePaymentProvider(): UsePaymentProviderInterface {
payment_method: {
...params.paymentMethod,
},
+ customQuery: params.customQuery,
};
result = await setPaymentMethodOnCartCommand.execute(context, paymentMethodParams);
@@ -52,13 +54,13 @@ export function usePaymentProvider(): UsePaymentProviderInterface {
return result;
};
- const load = async () => {
+ const load = async (customQuery?: CustomQuery) => {
Logger.debug('usePaymentProvider.load');
let result = null;
try {
loading.value = true;
- result = await getAvailablePaymentMethodsCommand.execute(context, cart.value.id);
+ result = await getAvailablePaymentMethodsCommand.execute(context, cart.value.id, customQuery);
error.value.load = null;
} catch (err) {
error.value.load = err;
diff --git a/packages/theme/modules/checkout/composables/usePaymentProvider/usePaymentProvider.ts b/packages/theme/modules/checkout/composables/usePaymentProvider/usePaymentProvider.ts
index 6dd972098..1987a8f12 100644
--- a/packages/theme/modules/checkout/composables/usePaymentProvider/usePaymentProvider.ts
+++ b/packages/theme/modules/checkout/composables/usePaymentProvider/usePaymentProvider.ts
@@ -1,11 +1,12 @@
import type { Ref } from '@nuxtjs/composition-api';
import type { ComposableFunctionArgs } from '~/composables/types';
import type { AvailablePaymentMethod, PaymentMethodInput } from '~/modules/GraphQL/types';
+import { CustomQuery } from '~/types/core';
-export interface PaymentMethodParams {
+export type PaymentMethodParams = ComposableFunctionArgs<{
cart_id: string;
payment_method: PaymentMethodInput;
-}
+}>;
/**
* The {@link usePaymentProvider} error object. The properties values' are the
@@ -39,7 +40,7 @@ export interface UsePaymentProviderInterface {
error: Readonly[>;
/** Loads the available payment methods for current cart. */
- load(): Promise];
+ load(customQuery?: CustomQuery): Promise;
/**
* Saves the payment method for current cart. It returns the updated available
diff --git a/packages/theme/modules/checkout/composables/useShippingProvider/commands/setShippingMethodsOnCartCommand.ts b/packages/theme/modules/checkout/composables/useShippingProvider/commands/setShippingMethodsOnCartCommand.ts
index c4c652707..471b539e7 100644
--- a/packages/theme/modules/checkout/composables/useShippingProvider/commands/setShippingMethodsOnCartCommand.ts
+++ b/packages/theme/modules/checkout/composables/useShippingProvider/commands/setShippingMethodsOnCartCommand.ts
@@ -1,9 +1,9 @@
-import { UseContextReturn } from '~/types/core';
+import { CustomQuery, UseContextReturn } from '~/types/core';
import type { SetShippingMethodsOnCartInput, Cart } from '~/modules/GraphQL/types';
export const setShippingMethodsOnCartCommand = {
- execute: async (context: UseContextReturn, shippingMethodParams: SetShippingMethodsOnCartInput): Promise => {
- const { data } = await context.app.$vsf.$magento.api.setShippingMethodsOnCart(shippingMethodParams);
+ execute: async (context: UseContextReturn, shippingMethodParams: SetShippingMethodsOnCartInput, customQuery: CustomQuery): Promise => {
+ const { data } = await context.app.$vsf.$magento.api.setShippingMethodsOnCart(shippingMethodParams, customQuery);
// TODO: Find out why 'Cart' doesn't match the type of the response data.
return (data?.setShippingMethodsOnCart?.cart as unknown as Cart) ?? null;
diff --git a/packages/theme/modules/checkout/composables/useShippingProvider/index.ts b/packages/theme/modules/checkout/composables/useShippingProvider/index.ts
index 628113a00..80e79323e 100644
--- a/packages/theme/modules/checkout/composables/useShippingProvider/index.ts
+++ b/packages/theme/modules/checkout/composables/useShippingProvider/index.ts
@@ -26,7 +26,7 @@ export function useShippingProvider(): UseShippingProviderInterface {
const { cart, setCart, load: loadCart } = useCart();
const context = useContext();
- const save = async ({ shippingMethod }: UseShippingProviderSaveParams) => {
+ const save = async ({ shippingMethod, customQuery = null }: UseShippingProviderSaveParams) => {
Logger.debug('useShippingProvider.save');
let result = null;
try {
@@ -37,7 +37,7 @@ export function useShippingProvider(): UseShippingProviderInterface {
shipping_methods: [shippingMethod],
};
- const cartData = await setShippingMethodsOnCartCommand.execute(context, shippingMethodParams);
+ const cartData = await setShippingMethodsOnCartCommand.execute(context, shippingMethodParams, customQuery);
Logger.debug('[Result]:', { cartData });
setCart(cartData);
diff --git a/packages/theme/modules/checkout/getters/cartGetters.ts b/packages/theme/modules/checkout/getters/cartGetters.ts
index 0b0302736..8819800d9 100644
--- a/packages/theme/modules/checkout/getters/cartGetters.ts
+++ b/packages/theme/modules/checkout/getters/cartGetters.ts
@@ -5,12 +5,12 @@ import type { Price } from '~/modules/catalog/types';
import type { ProductAttribute, Product } from '~/modules/catalog/product/types';
import { PaymentMethodInterface } from '~/modules/checkout/types';
import {
- CartItemInterface,
Cart,
Discount,
ProductInterface,
SelectedShippingMethod,
ConfigurableCartItem,
+ CartItemInterface,
} from '~/modules/GraphQL/types';
import { CartGetters as CartGettersBase, CartDiscount, Coupon } from '~/getters/types';
import { getName, getSlug as getSlugGetter, getProductThumbnailImage } from '~/modules/catalog/product/getters/productGetters';
diff --git a/packages/theme/modules/checkout/pages/Checkout.vue b/packages/theme/modules/checkout/pages/Checkout.vue
index 5b14e5baf..4c2000d50 100644
--- a/packages/theme/modules/checkout/pages/Checkout.vue
+++ b/packages/theme/modules/checkout/pages/Checkout.vue
@@ -44,7 +44,7 @@ import {
} from '@nuxtjs/composition-api';
import cartGetters from '~/modules/checkout/getters/cartGetters';
import useCart from '~/modules/checkout/composables/useCart';
-import CartPreview from '~/components/Checkout/CartPreview.vue';
+import CartPreview from '~/modules/checkout/components/CartPreview.vue';
export default defineComponent({
name: 'CheckoutPage',
diff --git a/packages/theme/modules/checkout/pages/Checkout/Billing.vue b/packages/theme/modules/checkout/pages/Checkout/Billing.vue
index 98d362670..01352270b 100644
--- a/packages/theme/modules/checkout/pages/Checkout/Billing.vue
+++ b/packages/theme/modules/checkout/pages/Checkout/Billing.vue
@@ -13,7 +13,7 @@
:label="$t('My billing and shipping address are the same')"
name="copyShippingAddress"
class="form__element"
- @change="handleCheckSameAddress"
+ @change="handleCheckSameAddress($event)"
/>
+ >
+
+ {{ shippingDetailsCountryName }}
+
+
@@ -34,12 +38,13 @@
v-if="!sameAsShipping && isAuthenticated && hasSavedBillingAddress"
v-model="setAsDefault"
v-e2e="'billing-addresses'"
- :current-address-id="currentAddressId || NOT_SELECTED_ADDRESS"
+ :current-address-id="currentAddressId"
:billing-addresses="addresses"
- @setCurrentAddress="handleSetCurrentAddress"
+ :countries="countries"
+ @setCurrentAddress="handleSetCurrentAddress($event)"
/>
changeBillingDetails('firstname', firstname)"
+ @input="changeBillingDetails('firstname', $event)"
/>
changeBillingDetails('lastname', lastname)"
+ @input="changeBillingDetails('lastname', $event)"
/>
changeBillingDetails('street', street)"
+ @input="changeBillingDetails('street', $event)"
/>
changeBillingDetails('apartment', apartment)"
+ @input="changeBillingDetails('apartment', $event)"
/>
changeBillingDetails('city', city)"
+ @input="changeBillingDetails('city', $event)"
/>
changeBillingDetails('region', region)"
+ @input="changeBillingDetails('region', $event)"
/>
changeBillingDetails('region', state)"
+ @input="changeBillingDetails('region', $event)"
>
+
+
changeBillingDetails('postcode', postcode)"
+ @input="changeBillingDetails('postcode', $event)"
/>
changeBillingDetails('telephone', telephone)"
+ @input="changeBillingDetails('telephone', $event)"
/>
import('~/components/Checkout/UserBillingAddresses.vue'),
+ UserBillingAddresses: () => import('~/modules/checkout/components/UserBillingAddresses.vue'),
UserAddressDetails,
},
setup() {
const router = useRouter();
const { app } = useContext();
const shippingDetails = ref(null);
- const billingAddress = ref(null);
const userBilling = ref(null);
const {
@@ -361,24 +364,25 @@ export default defineComponent({
const countries = ref([]);
const country = ref(null);
+
+ const shippingDetailsCountryName = computed(() => countries
+ .value
+ .find((countryItem) => countryItem.id === shippingDetails.value?.country.code)?.full_name_locale ?? '');
+
const { isAuthenticated } = useUser();
let oldBilling : CheckoutAddressForm | null = null;
const sameAsShipping = ref(false);
- const billingDetails = ref(
- billingAddress.value
- ? addressFromApiToForm(billingAddress.value)
- : getInitialCheckoutAddressForm(),
- );
- const currentAddressId = ref(NOT_SELECTED_ADDRESS);
+ const billingDetails = ref(getInitialCheckoutAddressForm());
+
+ const currentAddressId = ref(null);
const setAsDefault = ref(false);
const isFormSubmitted = ref(false);
- const canAddNewAddress = ref(true);
+ const isAddNewAddressFormVisible = ref(true);
const isBillingDetailsStepCompleted = ref(false);
const addresses = computed(() => (userBilling.value ? userBillingGetters.getAddresses(userBilling.value) : []));
const canMoveForward = computed(() => !loading.value && billingDetails.value && Object.keys(
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
billingDetails.value,
).length > 0);
@@ -389,7 +393,6 @@ export default defineComponent({
return addresses.value.length > 0;
});
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const countriesList = computed(() => addressGetter.countriesList(countries.value));
const regionInformation = computed(() => addressGetter.regionList(country.value));
@@ -398,12 +401,12 @@ export default defineComponent({
const billingDetailsData = {
billingDetails: {
...billingDetails.value,
- customerAddressId: addressId,
+ customerAddressId: addressId === null ? null : String(addressId),
sameAsShipping: sameAsShipping.value,
},
};
await save(billingDetailsData);
- if (addressId !== NOT_SELECTED_ADDRESS && setAsDefault.value) {
+ if (addressId !== null && setAsDefault.value) {
const [chosenAddress] = userBillingGetters.getAddresses(
userBilling.value,
{ id: addressId },
@@ -416,20 +419,20 @@ export default defineComponent({
}
reset();
await mergeItem('checkout', { billing: billingDetailsData });
- await router.push(`${app.localePath('/checkout/payment')}`);
+ await router.push(app.localeRoute({ name: 'payment' }));
isBillingDetailsStepCompleted.value = true;
};
- const handleCheckSameAddress = async () => {
- sameAsShipping.value = !sameAsShipping.value;
- if (sameAsShipping.value) {
+ const handleCheckSameAddress = async (value: boolean) => {
+ sameAsShipping.value = value;
+ if (value) {
shippingDetails.value = await loadShipping();
- country.value = await searchCountry({ id: (shippingDetails.value as ShippingCartAddress).country.code });
+ country.value = await searchCountry({ id: (shippingDetails.value).country.code });
oldBilling = { ...billingDetails.value };
billingDetails.value = {
...formatAddressReturnToData(shippingDetails.value),
};
- currentAddressId.value = NOT_SELECTED_ADDRESS;
+ currentAddressId.value = null;
setAsDefault.value = false;
if (billingDetails.value.country_code) {
country.value = await searchCountry({ id: billingDetails?.value.country_code });
@@ -443,85 +446,74 @@ export default defineComponent({
};
const handleAddNewAddressBtnClick = () => {
- currentAddressId.value = NOT_SELECTED_ADDRESS;
+ currentAddressId.value = null;
billingDetails.value = getInitialCheckoutAddressForm();
- canAddNewAddress.value = true;
+ isAddNewAddressFormVisible.value = true;
isBillingDetailsStepCompleted.value = false;
};
- const handleSetCurrentAddress = (addr: CustomerAddress) => {
- billingDetails.value = { ...addressFromApiToForm(addr) };
- currentAddressId.value = String(addr?.id);
- canAddNewAddress.value = false;
+ const handleSetCurrentAddress = async (customerAddress: CustomerAddress) => {
+ const id = customerAddress?.id;
+ currentAddressId.value = id;
+ if (id) {
+ isAddNewAddressFormVisible.value = false;
+ }
+ billingDetails.value = addressFromApiToForm(customerAddress);
+ country.value = customerAddress.country_code ? await searchCountry({ id: customerAddress.country_code }) : null;
isBillingDetailsStepCompleted.value = false;
};
- const changeBillingDetails = (field: string, value: unknown) => {
- billingDetails.value = {
- ...billingDetails.value,
- [field]: value,
- };
+ const changeBillingDetails = (field: keyof CheckoutAddressForm, value: string) => {
+ billingDetails.value[field] = value;
+ currentAddressId.value = null;
isBillingDetailsStepCompleted.value = false;
- currentAddressId.value = NOT_SELECTED_ADDRESS;
- };
-
- const selectDefaultAddress = () => {
- const defaultAddress = userBillingGetters.getAddresses(
- userBilling.value,
- { default_billing: true },
- );
- if (defaultAddress && defaultAddress.length > 0) {
- handleSetCurrentAddress(defaultAddress[0]);
- }
};
const changeCountry = async (id: string) => {
changeBillingDetails('country_code', id);
- country.value = await searchCountry({ id });
+ const newCountry = await searchCountry({ id });
+ billingDetails.value.region = '';
+ country.value = newCountry;
};
- watch(billingAddress, (addr) => {
- billingDetails.value = addr ? addressFromApiToForm(addr) : getInitialCheckoutAddressForm();
- });
-
onMounted(async () => {
- const validStep = await isPreviousStepValid('shipping');
+ const validStep = await isPreviousStepValid('user-account');
if (!validStep) {
- await router.push(app.localePath('/checkout/user-account'));
+ await router.push(app.localeRoute({ name: 'user-account' }));
}
+ const [loadedBillingInfoBoundToCart, loadedUserBilling, loadedCountries] = await Promise.all([
+ loadBilling(),
+ loadUserBilling(),
+ loadCountries(),
+ ]);
+ const [defaultAddress = null] = userBillingGetters.getAddresses(loadedUserBilling, { default_shipping: true });
+ const wasBillingAddressAlreadySetOnCart = Boolean(loadedBillingInfoBoundToCart);
+
+ // keep in mind default billing address is set on a customer's cart during cart creation
+ if (wasBillingAddressAlreadySetOnCart) {
+ const userAddressIdenticalToSavedCartAddress = findUserAddressIdenticalToSavedCartAddress(
+ loadedUserBilling?.addresses,
+ loadedBillingInfoBoundToCart,
+ );
- const [loadedCountries, loadedBilling] = await Promise.all([loadCountries(), loadBilling()]);
- countries.value = loadedCountries;
- billingAddress.value = loadedBilling;
-
+ handleSetCurrentAddress({ ...loadedBillingInfoBoundToCart, id: userAddressIdenticalToSavedCartAddress?.id });
+ } else if (defaultAddress) {
+ handleSetCurrentAddress(defaultAddress);
+ }
if (billingDetails.value?.country_code) {
country.value = await searchCountry({ id: billingDetails.value.country_code });
}
-
- if (!(userBilling.value as Customer)?.addresses && isAuthenticated.value) {
- userBilling.value = await loadUserBilling();
- }
- const billingAddresses = userBilling.value ? userBillingGetters.getAddresses(userBilling.value) : [];
-
- if (!billingAddresses || billingAddresses.length === 0) {
- return;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
- const hasEmptyBillingDetails = !billingDetails.value || Object.keys(billingDetails.value).length === 0;
- if (hasEmptyBillingDetails) {
- selectDefaultAddress();
- return;
- }
- canAddNewAddress.value = false;
+ userBilling.value = loadedUserBilling;
+ countries.value = loadedCountries;
});
return {
- canAddNewAddress,
+ isAddNewAddressFormVisible,
canMoveForward,
changeCountry,
changeBillingDetails,
countriesList,
+ countries,
country,
currentAddressId,
handleAddNewAddressBtnClick,
@@ -533,12 +525,12 @@ export default defineComponent({
isFormSubmitted,
isBillingDetailsStepCompleted,
loading,
- NOT_SELECTED_ADDRESS,
regionInformation,
searchCountry,
setAsDefault,
billingDetails,
sameAsShipping,
+ shippingDetailsCountryName,
addresses,
};
},
diff --git a/packages/theme/modules/checkout/pages/Checkout/Payment.vue b/packages/theme/modules/checkout/pages/Checkout/Payment.vue
index fe7da4295..6bb896ffc 100644
--- a/packages/theme/modules/checkout/pages/Checkout/Payment.vue
+++ b/packages/theme/modules/checkout/pages/Checkout/Payment.vue
@@ -68,10 +68,7 @@
@@ -178,6 +175,7 @@ import {
useContext,
onMounted,
} from '@nuxtjs/composition-api';
+
import cartGetters from '~/modules/checkout/getters/cartGetters';
import { useImage } from '~/composables';
import useMakeOrder from '~/modules/checkout/composables/useMakeOrder';
@@ -185,7 +183,7 @@ import useCart from '~/modules/checkout/composables/useCart';
import getShippingMethodPrice from '~/helpers/checkout/getShippingMethodPrice';
import { removeItem } from '~/helpers/asyncLocalStorage';
import { isPreviousStepValid } from '~/helpers/checkout/steps';
-import type { BundleCartItem, ConfigurableCartItem } from '~/modules/GraphQL/types';
+import type { BundleCartItem, ConfigurableCartItem, CartItemInterface } from '~/modules/GraphQL/types';
export default defineComponent({
name: 'ReviewOrderAndPayment',
@@ -199,7 +197,7 @@ export default defineComponent({
SfProperty,
SfLink,
SfImage,
- VsfPaymentProvider: () => import('~/components/Checkout/VsfPaymentProvider.vue'),
+ VsfPaymentProvider: () => import('~/modules/checkout/components/VsfPaymentProvider.vue'),
},
setup() {
const order = ref(null);
@@ -243,7 +241,7 @@ export default defineComponent({
);
const { getMagentoImage, imageSizes } = useImage();
-
+ const getRowTotal = (product: CartItemInterface) => cartGetters.getItemPrice(product).regular - cartGetters.getItemPrice(product).special;
return {
cart,
cartGetters,
@@ -263,6 +261,7 @@ export default defineComponent({
getBundles,
getMagentoImage,
imageSizes,
+ getRowTotal,
};
},
});
diff --git a/packages/theme/modules/checkout/pages/Checkout/Shipping.vue b/packages/theme/modules/checkout/pages/Checkout/Shipping.vue
index 8ee844561..c0908df26 100644
--- a/packages/theme/modules/checkout/pages/Checkout/Shipping.vue
+++ b/packages/theme/modules/checkout/pages/Checkout/Shipping.vue
@@ -3,21 +3,22 @@
@@ -264,7 +261,6 @@ import {
import {
ref,
computed,
- watch,
onMounted,
defineComponent,
useRouter,
@@ -274,21 +270,19 @@ import { required, min, digits } from 'vee-validate/dist/rules';
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate';
import userShippingGetters from '~/modules/customer/getters/userShippingGetters';
import addressGetter from '~/modules/customer/getters/addressGetter';
-import {
- useCountrySearch,
-} from '~/composables';
+import { useCountrySearch } from '~/composables';
import type {
- Country, AvailableShippingMethod, ShippingCartAddress, CustomerAddress, Customer,
+ Country, AvailableShippingMethod, CustomerAddress, Customer,
} from '~/modules/GraphQL/types';
import useShipping from '~/modules/checkout/composables/useShipping';
import useUser from '~/modules/customer/composables/useUser';
import useUserAddress from '~/modules/customer/composables/useUserAddress';
-import { addressFromApiToForm, CheckoutAddressForm, getInitialCheckoutAddressForm } from '~/helpers/checkout/address';
+import {
+ addressFromApiToForm, CheckoutAddressForm, findUserAddressIdenticalToSavedCartAddress, getInitialCheckoutAddressForm,
+} from '~/helpers/checkout/address';
import { mergeItem } from '~/helpers/asyncLocalStorage';
import { isPreviousStepValid } from '~/helpers/checkout/steps';
-const NOT_SELECTED_ADDRESS = '';
-
extend('required', {
...required,
message: 'This field is required',
@@ -311,13 +305,12 @@ export default defineComponent({
SfSelect,
ValidationProvider,
ValidationObserver,
- UserShippingAddresses: () => import('~/components/Checkout/UserShippingAddresses.vue'),
- VsfShippingProvider: () => import('~/components/Checkout/VsfShippingProvider.vue'),
+ UserShippingAddresses: () => import('~/modules/checkout/components/UserShippingAddresses.vue'),
+ VsfShippingProvider: () => import('~/modules/checkout/components/VsfShippingProvider.vue'),
},
setup() {
const router = useRouter();
const { app } = useContext();
- const address = ref(null);
const userShipping = ref(null);
const {
load: loadShipping,
@@ -336,13 +329,13 @@ export default defineComponent({
const countries = ref([]);
const country = ref(null);
const { isAuthenticated } = useUser();
- const shippingDetails = ref(address.value ? addressFromApiToForm(address.value) : getInitialCheckoutAddressForm());
+ const shippingDetails = ref(getInitialCheckoutAddressForm());
const shippingMethods = ref([]);
- const currentAddressId = ref(NOT_SELECTED_ADDRESS);
+ const currentAddressId = ref(null);
- const setAsDefault = ref(false);
+ const isSetAsDefaultRequested = ref(false);
const isFormSubmitted = ref(false);
- const canAddNewAddress = ref(true);
+ const isAddNewAddressFormVisible = ref(true);
const isShippingDetailsStepCompleted = ref(false);
const addresses = computed(() => userShippingGetters.getAddresses(userShipping.value));
@@ -373,12 +366,12 @@ export default defineComponent({
const shippingInfo = await saveShipping({ shippingDetails: shippingDetailsData });
shippingMethods.value = shippingInfo?.available_shipping_methods ?? [];
- if (addressId !== NOT_SELECTED_ADDRESS && setAsDefault.value) {
+ if (addressId !== null && isSetAsDefaultRequested.value) {
const [chosenAddress] = userShippingGetters.getAddresses(
userShipping.value,
{ id: addressId },
);
- chosenAddress.default_shipping = setAsDefault.value;
+ chosenAddress.default_shipping = isSetAsDefaultRequested.value;
if (chosenAddress) {
await setDefaultAddress({ address: chosenAddress });
userShipping.value = await loadUserShipping(true);
@@ -389,85 +382,72 @@ export default defineComponent({
};
const handleAddNewAddressBtnClick = () => {
- currentAddressId.value = NOT_SELECTED_ADDRESS;
+ currentAddressId.value = null;
shippingDetails.value = getInitialCheckoutAddressForm();
- canAddNewAddress.value = true;
+ isAddNewAddressFormVisible.value = true;
isShippingDetailsStepCompleted.value = false;
};
- const handleSetCurrentAddress = (addr: CustomerAddress) => {
- shippingDetails.value = { ...addressFromApiToForm(addr) };
- currentAddressId.value = String(addr?.id);
- canAddNewAddress.value = false;
+ const handleSetCurrentAddress = async (customerAddress: CustomerAddress) => {
+ const id = customerAddress?.id;
+ currentAddressId.value = id;
+ if (id) {
+ isAddNewAddressFormVisible.value = false;
+ }
+ shippingDetails.value = addressFromApiToForm(customerAddress);
+ country.value = customerAddress.country_code ? await searchCountry({ id: customerAddress.country_code }) : null;
isShippingDetailsStepCompleted.value = false;
};
- const changeShippingDetails = (field: string, value: unknown) => {
- shippingDetails.value = {
- ...shippingDetails.value,
- [field]: value,
- };
+ const changeShippingDetails = (field: keyof CheckoutAddressForm, value: string) => {
+ shippingDetails.value[field] = value;
isShippingDetailsStepCompleted.value = false;
- currentAddressId.value = NOT_SELECTED_ADDRESS;
- };
-
- const selectDefaultAddress = () => {
- const defaultAddress = userShippingGetters.getAddresses(
- userShipping.value,
- { default_shipping: true },
- ) as [CustomerAddress] | [];
- if (defaultAddress && defaultAddress.length > 0) {
- handleSetCurrentAddress(defaultAddress[0]);
- }
+ currentAddressId.value = null;
};
const changeCountry = async (id: string) => {
changeShippingDetails('country_code', id);
- country.value = await searchCountry({ id });
+ const newCountry = await searchCountry({ id });
+ shippingDetails.value.region = '';
+ country.value = newCountry;
};
- watch(address, (addr) => {
- shippingDetails.value = addr ? addressFromApiToForm(addr) : getInitialCheckoutAddressForm();
- });
onMounted(async () => {
const validStep = await isPreviousStepValid('user-account');
if (!validStep) {
- await router.push(app.localePath('/checkout/user-account'));
+ await router.push(app.localeRoute({ name: 'user-account' }));
}
- const [loadedShippingInfo, loadedUserShipping, loadedCountries] = await Promise.all([
+ const [loadedShippingInfoBoundToCart, loadedUserShipping, loadedCountries] = await Promise.all([
loadShipping(),
loadUserShipping(),
loadCountries(),
]);
- address.value = loadedShippingInfo;
- userShipping.value = loadedUserShipping;
- countries.value = loadedCountries;
+ const [defaultAddress = null] = userShippingGetters.getAddresses(loadedUserShipping, { default_shipping: true });
+ const wasShippingAddressAlreadySetOnCart = Boolean(loadedShippingInfoBoundToCart);
+ if (wasShippingAddressAlreadySetOnCart) {
+ const userAddressIdenticalToSavedCartAddress = findUserAddressIdenticalToSavedCartAddress(
+ loadedUserShipping?.addresses,
+ loadedShippingInfoBoundToCart,
+ );
+ handleSetCurrentAddress({ ...loadedShippingInfoBoundToCart, id: userAddressIdenticalToSavedCartAddress?.id });
+ } else if (defaultAddress) {
+ handleSetCurrentAddress(defaultAddress);
+ }
if (shippingDetails.value?.country_code) {
country.value = await searchCountry({ id: shippingDetails.value.country_code });
}
-
- const shippingAddresses = userShippingGetters.getAddresses(
- userShipping.value,
- );
-
- if (!shippingAddresses || shippingAddresses.length === 0) {
- return;
- }
-
- const hasEmptyShippingDetails = !shippingDetails.value
- || Object.keys(shippingDetails.value).length === 0;
- if (hasEmptyShippingDetails) {
- selectDefaultAddress();
- }
+ userShipping.value = loadedUserShipping;
+ countries.value = loadedCountries;
});
return {
- canAddNewAddress,
+ isAddNewAddressFormVisible,
canMoveForward,
changeCountry,
changeShippingDetails,
+ countries,
countriesList,
country,
currentAddressId,
@@ -479,10 +459,9 @@ export default defineComponent({
isFormSubmitted,
isShippingDetailsStepCompleted,
isShippingLoading,
- NOT_SELECTED_ADDRESS,
regionInformation,
searchCountry,
- setAsDefault,
+ isSetAsDefaultRequested,
shippingDetails,
shippingMethods,
addresses,
diff --git a/packages/theme/modules/checkout/pages/Checkout/UserAccount.vue b/packages/theme/modules/checkout/pages/Checkout/UserAccount.vue
index 0ebc0d889..a6939fec3 100644
--- a/packages/theme/modules/checkout/pages/Checkout/UserAccount.vue
+++ b/packages/theme/modules/checkout/pages/Checkout/UserAccount.vue
@@ -147,9 +147,7 @@ import {
required, min, email,
} from 'vee-validate/dist/rules';
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate';
-import {
- useUiNotification, useGuestUser,
-} from '~/composables';
+import { useGuestUser } from '~/composables';
import useCart from '~/modules/checkout/composables/useCart';
import { useUser } from '~/modules/customer/composables/useUser';
import { getItem, mergeItem } from '~/helpers/asyncLocalStorage';
@@ -208,15 +206,13 @@ export default defineComponent({
error: errorUser,
} = useUser();
- const { send: sendNotification } = useUiNotification();
-
const isFormSubmitted = ref(false);
const createUserAccount = ref(false);
const loginUserAccount = ref(false);
const loading = computed(() => loadingUser.value || loadingGuestUser.value);
const canMoveForward = computed(() => !(loading.value));
- const hasError = computed(() => errorUser.value.register || errorGuestUser.value.attachToCart);
+ const anyError = computed(() => errorUser.value.login || errorUser.value.register || errorGuestUser.value.attachToCart);
type Form = {
firstname: string,
@@ -249,9 +245,9 @@ export default defineComponent({
}
await (
- !createUserAccount.value
- ? attachToCart({ email: form.value.email, cart })
- : register({ user: form.value })
+ createUserAccount.value
+ ? register({ user: form.value })
+ : attachToCart({ email: form.value.email, cart })
);
}
@@ -270,24 +266,14 @@ export default defineComponent({
});
}
- if (!hasError.value) {
+ if (!anyError.value) {
await mergeItem('checkout', { 'user-account': form.value });
- await router.push(`${app.localePath('/checkout/shipping')}`);
+ await router.push(app.localeRoute({ name: 'shipping' }));
reset();
isFormSubmitted.value = true;
- } else {
- sendNotification({
- id: Symbol('user_form_error'),
- message: 'Something went wrong during form submission. Please try again later',
- type: 'danger',
- icon: 'error',
- persist: false,
- title: 'Error',
- });
}
if (isRecaptchaEnabled.value) {
- // reset recaptcha
$recaptcha.reset();
}
};
@@ -332,7 +318,6 @@ export default defineComponent({