├── .gitignore
├── Controller
└── Cart
│ └── Sync.php
├── LICENSE
├── Model
└── Config.php
├── Observer
└── SuccessObserver.php
├── README.md
├── Service
├── Sync.php
├── SyncInterface.php
└── SyncLoggerFactory.php
├── composer.json
├── etc
├── acl.xml
├── adminhtml
│ └── system.xml
├── di.xml
├── events.xml
├── frontend
│ └── routes.xml
└── module.xml
└── registration.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .php_cs.cache
2 | .idea/*
3 | *~
--------------------------------------------------------------------------------
/Controller/Cart/Sync.php:
--------------------------------------------------------------------------------
1 | customerRepository = $customerRepository;
76 | $this->customerSession = $customerSession;
77 | $this->tokenFactory = $tokenFactory;
78 | $this->sync = $sync;
79 | $this->logger = $syncLoggerFactory->create();
80 | $this->config = $config;
81 | }
82 |
83 | /**
84 | * @return ResponseInterface|Json|ResultInterface
85 | */
86 | public function execute()
87 | {
88 | $checkoutPath = $this->config->getCheckoutPath();
89 |
90 | if (!$this->hasRequestAllRequiredParams()) {
91 | return $this->resultRedirectFactory->create()->setPath($checkoutPath);
92 | }
93 |
94 | $customerToken = $this->getRequest()->getParam('token');
95 | $cartId = $this->getRequest()->getParam('cart');
96 |
97 | /** @var Token $token */
98 | $token = $this->tokenFactory->create()->loadByToken($customerToken);
99 |
100 | if ($this->isGuestCart($token)) {
101 | if ($this->customerSession->isLoggedIn()) {
102 | $guestToken = md5(microtime() . mt_rand());
103 |
104 | return $this->logoutCustomer($guestToken, $cartId);
105 | }
106 |
107 | $this->sync->synchronizeGuestCart($cartId);
108 | } else {
109 | $isCustomerLogged = false;
110 |
111 | if ($this->customerSession->isLoggedIn()) {
112 | $isCustomerLogged = true;
113 |
114 | if ($token->getCustomerId() !== $this->customerSession->getCustomerId()) {
115 | return $this->logoutCustomer($customerToken, $cartId);
116 | }
117 | }
118 |
119 | if (!$isCustomerLogged) {
120 | try {
121 | $customer = $this->customerRepository->getById($token->getCustomerId());
122 | } catch (NoSuchEntityException $e) {
123 | $this->logger->addError($e->getMessage());
124 | $this->messageManager->addErrorMessage(__('Required customer doesn\'t exist'));
125 |
126 | return $this->resultRedirectFactory->create()->setPath($checkoutPath);
127 | } catch (LocalizedException $e) {
128 | $this->logger->addError($e->getMessage());
129 | $this->messageManager->addErrorMessage(__('Cannot synchronize customer cart'));
130 |
131 | return $this->resultRedirectFactory->create()->setPath($checkoutPath);
132 | }
133 |
134 | $this->customerSession->loginById($customer->getId());
135 | }
136 |
137 | $this->sync->synchronizeCustomerCart($this->customerSession->getCustomerId(), $cartId);
138 | }
139 |
140 | return $this->resultRedirectFactory->create()->setPath($checkoutPath);
141 | }
142 |
143 | /**
144 | * @return bool
145 | */
146 | private function hasRequestAllRequiredParams(): bool
147 | {
148 | return null !== $this->getRequest()->getParam('token')
149 | && !empty($this->getRequest()->getParam('cart'));
150 | }
151 |
152 | /**
153 | * @param Token $token
154 | *
155 | * @return bool
156 | */
157 | private function isCustomerCart(Token $token): bool
158 | {
159 | return $this->isCustomerToken($token);
160 | }
161 |
162 | /**
163 | * @param Token $token
164 | *
165 | * @return bool
166 | */
167 | private function isGuestCart(Token $token): bool
168 | {
169 | return !$this->isCustomerCart($token);
170 | }
171 |
172 | /**
173 | * @param Token $token
174 | *
175 | * @return bool
176 | */
177 | private function isCustomerToken(Token $token): bool
178 | {
179 | return $token->getId() && !$token->getRevoked() && $token->getCustomerId();
180 | }
181 |
182 | /**
183 | * @param string $customerToken
184 | * @param string $cartId
185 | *
186 | * @return ResponseInterface
187 | */
188 | private function logoutCustomer(string $customerToken, string $cartId): ResponseInterface
189 | {
190 | $this->customerSession->logout();
191 |
192 | return $this->_redirect(
193 | 'vue/cart/sync',
194 | [
195 | 'token' => $customerToken,
196 | 'cart' => $cartId,
197 | ]
198 | );
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Divante Ltd.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Model/Config.php:
--------------------------------------------------------------------------------
1 | scopeConfig = $scopeConfig;
23 | }
24 |
25 | public function getCheckoutPath()
26 | {
27 | $value = $this->getConfigValue(self::CHECKOUT_PATH);
28 | if (!$value) {
29 | return 'checkout/cart';
30 | }
31 | return trim($value);
32 | }
33 |
34 | public function getVueStorefrontSuccessUrl()
35 | {
36 | return $this->getConfigValue(self::VUESTOREFRONT_SUCCES_PATH);
37 | }
38 |
39 | protected function getConfigValue($key)
40 | {
41 | return $this->scopeConfig->getValue($key, ScopeInterface::SCOPE_STORE);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Observer/SuccessObserver.php:
--------------------------------------------------------------------------------
1 | config = $config;
25 | }
26 |
27 | /**
28 | * @param \Magento\Framework\Event\Observer $observer
29 | */
30 | public function execute(\Magento\Framework\Event\Observer $observer)
31 | {
32 | $url = $this->config->getVueStorefrontSuccessUrl();
33 |
34 | if ($url && $url !== '') {
35 | if (!(strpos($url, "http://") !== false || strpos($url, "https://") !== false)) {
36 | $url = 'https://' . $url;
37 | }
38 | header("Location: " . $url);
39 | die();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Magento 2 External Checkout for Vue Storefront
2 | **This module is for Magento 2 only. You can find the Magento 1 external checkout module [here](https://github.com/DivanteLtd/magento1-external-checkout)**
3 |
4 | This Magento extension allow You to merge given shopping cart with current's user session. It performs a auto-login if user-token provided.
5 |
6 | This module is designed to work with: [Vue Storefront External Checkout](https://github.com/Vendic/vsf-external-checkout).
7 |
8 | This extension allows the user to start the session within the Vue Storefront shop and finalize the order in Magento2. It's great when You have very extended/customized Magento checkout which will be hard to port to Vue Storefront.
9 |
10 | 
11 |
12 | ### Demo
13 | Check [meubelplaats.nl](https://www.meubelplaats.nl) for a demo of this module. Once go to the checkout you will be redirected to Magento 2, to finalize your order there.
14 | ### Compatibility
15 | - Magento 2.2 or Magento 2.3
16 |
17 | ### Prerequisites (the Vue Storefront part)
18 | 1. Integrate Your Magento2 instance with Vue Storefront: [tutorial](https://medium.com/@piotrkarwatka/vue-storefront-cart-totals-orders-integration-with-magento2-6fbe6860fcd), [video tutorial](https://www.youtube.com/watch?v=CtDXddsyxvM)
19 | 2. Install [Vue Storefront External Checkout](https://github.com/Vendic/vsf-external-checkout) on your Vue Storefront instance
20 |
21 | ### Installation guide (the Magento 2 part)
22 | 1. Install the module with composer:
23 | ```bash
24 | composer require vuestorefront/magento2-vue-cart-sync
25 | ```
26 | 3. Run `php bin/magento setup:upgrade`
27 | 4. Please install the [`vsf-external-checkout`](https://github.com/Vendic/vsf-external-checkout) module for Vue Storefront. [See the instruction](https://github.com/Vendic/vsf-external-checkout).
28 | 5. Go to: Stores -> Configuration | VueStorefront -> External Checkout and set URL
29 |
30 | To test if Your extension works just fine, You can test the following URL:
31 | * http://your-base-magento-address.io/vue/cart/sync/token/{customer-api-token}/cart/{cartId}
32 |
33 | For example, our test address looks like:
34 | * http://demo-magento2.vuestorefront.io/vue/cart/sync/token/s7nirf24cxro7qx1hb9uujaq4jx97nvp/cart/3648
35 |
36 | where
37 | * `s7nirf24cxro7qx1hb9uujaq4jx97nvp` is a customer token provided by [`POST /V1/integration/customer/token`](http://devdocs.magento.com/guides/v2.0/get-started/authentication/gs-authentication-token.html) or can be empty!
38 | * `3648` is a quote id; for guest-carts it will be not integer but guid string
39 |
40 | ## Credits
41 |
42 | Mateusz Bukowski (@gatzzu)
43 |
--------------------------------------------------------------------------------
/Service/Sync.php:
--------------------------------------------------------------------------------
1 | cartRepository = $cartRepository;
82 | $this->checkoutSession = $checkoutSession;
83 | $this->customerRepository = $customerRepository;
84 | $this->messageManager = $messageManager;
85 | $this->quoteIdMaskFactory = $quoteIdMaskFactory;
86 | $this->quoteFactory = $quoteFactory;
87 | $this->quoteRepository = $quoteRepository;
88 | $this->logger = $syncLoggerFactory->create();
89 | }
90 |
91 | /**
92 | * @param int $customerId
93 | * @param int $cartId
94 | *
95 | * @return SyncInterface|false
96 | */
97 | public function synchronizeCustomerCart($customerId, $cartId)
98 | {
99 | if (!is_numeric($cartId)) {
100 | $cartId = $this->getGuestQuoteId($cartId);
101 |
102 | if (null === $cartId) {
103 | $this->messageManager->addErrorMessage(__('Guest quote doen\'t exists'));
104 |
105 | return false;
106 | }
107 | }
108 |
109 | try {
110 | $customer = $this->customerRepository->getById($customerId);
111 | } catch (NoSuchEntityException $e) {
112 | $this->messageManager->addErrorMessage(__('This customer doen\'t exists'));
113 |
114 | return false;
115 | } catch (LocalizedException $e) {
116 | $this->logger->addError($e->getMessage());
117 |
118 | return false;
119 | }
120 |
121 | try {
122 | $customerQuote = $this->quoteRepository->getForCustomer($customer->getId());
123 | } catch (NoSuchEntityException $e) {
124 | $customerQuote = $this->quoteFactory->create();
125 | }
126 |
127 | $customerQuote->setStoreId($customer->getStoreId());
128 |
129 | try {
130 | $vueQuote = $this->quoteRepository->getActive($cartId);
131 | } catch (NoSuchEntityException $e) {
132 | $this->logger->addError($e->getMessage());
133 |
134 | return false;
135 | }
136 |
137 | if ($customerQuote->getId() && $vueQuote->getId() !== $customerQuote->getId()) {
138 | $vueQuote->assignCustomerWithAddressChange(
139 | $customer,
140 | $vueQuote->getBillingAddress(),
141 | $vueQuote->getShippingAddress()
142 | );
143 | $this->quoteRepository->save($vueQuote->merge($customerQuote)->collectTotals());
144 | $this->checkoutSession->replaceQuote($vueQuote);
145 | $this->quoteRepository->delete($customerQuote);
146 | $this->checkoutSession->regenerateId();
147 | } else {
148 | $customerQuote->assignCustomerWithAddressChange(
149 | $customer,
150 | $customerQuote->getBillingAddress(),
151 | $customerQuote->getShippingAddress()
152 | );
153 | $customerQuote->collectTotals();
154 | $this->quoteRepository->save($customerQuote);
155 | $this->checkoutSession->replaceQuote($customerQuote);
156 | }
157 |
158 | return $this;
159 | }
160 |
161 | /**
162 | * @param string $cartId
163 | *
164 | * @return SyncInterface|false
165 | */
166 | public function synchronizeGuestCart(string $cartId)
167 | {
168 | $quoteIdMask = $this->getGuestQuoteId($cartId);
169 |
170 | if (null === $quoteIdMask) {
171 | $this->messageManager->addErrorMessage(__('Guest quote doen\'t exists'));
172 |
173 | return false;
174 | }
175 |
176 | try {
177 | $quote = $this->quoteRepository->getActive($quoteIdMask);
178 | } catch (NoSuchEntityException $e) {
179 | $this->messageManager->addErrorMessage(__('Guest quote doen\'t exists'));
180 |
181 | return false;
182 | }
183 |
184 | $this->cartRepository->save($quote->collectTotals());
185 | $this->checkoutSession->replaceQuote($quote);
186 | $this->checkoutSession->regenerateId();
187 |
188 | return $this;
189 | }
190 |
191 | /**
192 | * @param string $cartId
193 | *
194 | * @return int|null
195 | */
196 | private function getGuestQuoteId(string $cartId)
197 | {
198 | /** @var QuoteIdMask $quoteIdMask */
199 | $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id');
200 |
201 | return $quoteIdMask->getQuoteId();
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/Service/SyncInterface.php:
--------------------------------------------------------------------------------
1 | pushHandler(new StreamHandler(static::$path));
29 |
30 | return $logger;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuestorefront/magento2-vue-cart-sync",
3 | "description": "Synchronize Magento cart between Vue Storefront",
4 | "require": {
5 | "php": "~7.1|~7.2",
6 | "magento/magento2-base": "~2.2|~2.3"
7 | },
8 | "type": "magento2-module",
9 | "version": "2.0.0",
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Mateusz Bukowski",
14 | "email": "mbukowski@divante.pl"
15 | },
16 | {
17 | "name": "Tjitse Efde",
18 | "email": "tjitse@vendic.nl"
19 | }
20 | ],
21 | "autoload": {
22 | "files": [
23 | "registration.php"
24 | ],
25 | "psr-4": {
26 | "VueStorefront\\CartSync\\": ""
27 | }
28 | },
29 | "suggest": {
30 | "vendic/magento2-checkoutonly": "A module that enables you to limit access only the Magento checkout. Usefull for a PWA setup that uses the default Magento 2 checkout"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/etc/acl.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/etc/adminhtml/system.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
10 |
11 | vuestorefront_config
12 | VueStorefront_CartSync::configuration
13 |
15 |
16 |
18 |
19 | After a transaction, we will redirect the traffic to this URL. For example:
20 | 'https://demo.vuestorefront.io/succespage'
21 |
22 |
23 |
25 |
26 | We will redirect the traffic from VueStorefront to this path. For example 'checkout/cart' or 'checkout'
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/etc/di.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/etc/events.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/etc/frontend/routes.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/etc/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/registration.php:
--------------------------------------------------------------------------------
1 |