├── .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 | ![External checkout for Vue Storefront](https://raw.githubusercontent.com/Vendic/vsf-external-checkout/master/media/diagram.png) 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 |