diff --git a/Model/Checks/HasActiveSavedCreditCards.php b/Model/Checks/HasActiveSavedCreditCards.php new file mode 100644 index 0000000..b1723d6 --- /dev/null +++ b/Model/Checks/HasActiveSavedCreditCards.php @@ -0,0 +1,92 @@ +paymentTokenCollection = $paymentTokenCollection; + $this->dateTimeFactory = $dateTimeFactory; + } + + /** + * @inheritDoc + */ + public function isApplicable(MethodInterface $paymentMethod, Quote $quote) + { + if ($paymentMethod->getCode() !== ConfigProvider::CC_VAULT_CODE) { + return true; + } + + $customerId = $quote->getCustomerId(); + + return $customerId !== null && $this->hasActiveSavedCards($customerId); + } + + /** + * Checks if customer has active saved credit cards. + * + * @param int $customerId + * @return bool + */ + private function hasActiveSavedCards($customerId) + { + $this->paymentTokenCollection->addFilter(PaymentTokenInterface::CUSTOMER_ID, $customerId); + $this->paymentTokenCollection + ->addFilter(PaymentTokenInterface::PAYMENT_METHOD_CODE, ConfigProvider::CODE); + $this->paymentTokenCollection->addFilter(PaymentTokenInterface::IS_ACTIVE, 1); + $this->paymentTokenCollection->addFilter(PaymentTokenInterface::IS_VISIBLE, 1); + $this->paymentTokenCollection->addFieldToFilter( + PaymentTokenInterface::EXPIRES_AT, + [ + 'gt' => $this->dateTimeFactory->create( + 'now', + new DateTimeZone('UTC') + )->format('Y-m-d 00:00:00') + ] + ); + return $this->paymentTokenCollection->count() > 0; + } +} diff --git a/Model/CreatePaymentIntent.php b/Model/CreatePaymentIntent.php index a7d2650..70c7203 100644 --- a/Model/CreatePaymentIntent.php +++ b/Model/CreatePaymentIntent.php @@ -15,7 +15,15 @@ */ namespace TNW\Stripe\Model; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\State\InputMismatchException; +use Magento\Quote\Model\Quote; use Magento\Vault\Api\PaymentTokenManagementInterface; +use Stripe\Exception\ApiErrorException; +use Stripe\PaymentIntent; use TNW\Stripe\Helper\Payment\Formatter; use TNW\Stripe\Model\Adapter\StripeAdapterFactory; use TNW\Stripe\Helper\Customer as CustomerHelper; @@ -64,40 +72,56 @@ class CreatePaymentIntent */ private $vaultTokenProcessor; + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + /** * @param StripeAdapterFactory $adapterFactory * @param CustomerHelper $customerHelper * @param UrlInterface $url * @param PaymentTokenManagementInterface $tokenManagement * @param VaultTokenProcessor $vaultTokenProcessor + * @param CustomerRepositoryInterface $customerRepository */ public function __construct( StripeAdapterFactory $adapterFactory, CustomerHelper $customerHelper, UrlInterface $url, PaymentTokenManagementInterface $tokenManagement, - VaultTokenProcessor $vaultTokenProcessor + VaultTokenProcessor $vaultTokenProcessor, + CustomerRepositoryInterface $customerRepository ) { $this->vaultTokenProcessor = $vaultTokenProcessor; $this->url = $url; $this->adapterFactory = $adapterFactory; $this->customerHelper = $customerHelper; $this->tokenManagement = $tokenManagement; + $this->customerRepository = $customerRepository; } /** * @param $data - * @param \Magento\Quote\Model\Quote $quote + * @param Quote $quote * @param bool $isLoggedIn - * @return \Stripe\PaymentIntent - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException - * @throws \Magento\Framework\Exception\State\InputMismatchException - * @throws \Stripe\Exception\ApiErrorException + * @return PaymentIntent + * @throws LocalizedException + * @throws InputException + * @throws NoSuchEntityException + * @throws InputMismatchException + * @throws ApiErrorException */ public function getPaymentIntent($data, $quote, $isLoggedIn = false) { + $customer = $quote->getCustomer(); + if (!$customer->getId() && $quote->getCustomerEmail() + && $this->isExistingCustomerEmail($quote->getCustomerEmail(), $customer->getWebsiteId()) + ) { + throw new LocalizedException( + __('A customer with the same email address already exists in an associated website.') + ); + } $stripeAdapter = $this->adapterFactory->create(); if (property_exists($data, 'public_hash')) { $customerId = $quote->getCustomer()->getId(); @@ -116,7 +140,7 @@ public function getPaymentIntent($data, $quote, $isLoggedIn = false) 'payment_method' => $payment, 'metadata' => ['site' => $this->url->getBaseUrl()] ]; - $email = $quote->getBillingAddress()->getEmail(); + $email = $quote->getCustomerEmail(); if (!$isLoggedIn) { $attributes['description'] = 'guest'; } @@ -149,4 +173,22 @@ public function getPaymentIntent($data, $quote, $isLoggedIn = false) } return $stripeAdapter->createPaymentIntent($params); } + + /** + * Checks if customer with given email exists + * + * @param string $email + * @param int|null $websiteId + * @return bool + * @throws LocalizedException + */ + public function isExistingCustomerEmail($email, $websiteId = null) + { + try { + $this->customerRepository->get($email, $websiteId); + return true; + } catch (NoSuchEntityException $e) { + return false; + } + } } diff --git a/composer.json b/composer.json index 7bbe60a..5ed1b0d 100755 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "tnw/module-stripe", "description": "Stripe Payments for Magento 2", "type": "magento2-module", - "version": "2.3.11", + "version": "2.3.12", "license": "OSL-3.0", "require": { "magento/framework": ">100", diff --git a/etc/csp_whitelist.xml b/etc/csp_whitelist.xml new file mode 100644 index 0000000..3c8e67f --- /dev/null +++ b/etc/csp_whitelist.xml @@ -0,0 +1,15 @@ + + + + + + https://*.stripe.com + + + + + https://*.stripe.com + + + + diff --git a/etc/di.xml b/etc/di.xml index 21a24f0..2264ad0 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -393,4 +393,20 @@ type="TNW\Stripe\Plugin\Quote\Api\CartManagement" sortOrder="1"/> + + + + + TNW\Stripe\Model\Checks\HasActiveSavedCreditCards + + + + + + + + has_saved_credit_cards + + + diff --git a/view/adminhtml/web/js/stripe.js b/view/adminhtml/web/js/stripe.js index 897aa64..7a983e4 100644 --- a/view/adminhtml/web/js/stripe.js +++ b/view/adminhtml/web/js/stripe.js @@ -176,6 +176,7 @@ define([ }.bind(this), 1000) } this.$selector.on('submitOrder.tnw_stripe', this.submitOrder.bind(this)); + this.$selector.on('beforeSubmitOrder', this.beforeSubmitOrder.bind(this)); }, /** @@ -184,12 +185,26 @@ define([ disableEventListeners: function () { this.$selector.off('submitOrder'); this.$selector.off('submit'); + this.$selector.off('beforeSubmitOrder'); + }, + + /** + * Event handler for 'beforeSubmitOrder' event + * @param event + */ + beforeSubmitOrder: function (event) { + if (this.active() && (this.getPaymentMethodToken() || this.isCreatingPaymentIntent || this.isSubmitting)) { + // Cancel order submission if payment intent is already created + event.result = false; + this.$selector.trigger('processStop'); + } }, /** * Trigger order submit */ submitOrder: function () { + this.isSubmitting = true; var self = this; this.$selector.validate().form(); this.$selector.trigger('afterValidate.beforeSubmit'); @@ -250,6 +265,9 @@ define([ self.error('Something went wrong') $('body').trigger('processStop'); }) + .always(function () { + self.isSubmitting = false; + }); }, /** @@ -279,6 +297,7 @@ define([ * @returns {jQuery.Deferred} */ createPaymentIntent: function () { + this.isCreatingPaymentIntent = true; var self = this, dfd = $.Deferred(); if ($("#tnw_stripe_vault").length) { @@ -294,6 +313,8 @@ define([ } else { dfd.resolve(response); } + }).always(function () { + self.isCreatingPaymentIntent = false; }); return dfd; }, @@ -335,6 +356,14 @@ define([ $('#' + this.container).find('#' + this.code + '_cc_token').val(token); }, + /** + * Get payment method token + * @return {string} + */ + getPaymentMethodToken: function () { + return $('#' + this.container).find('#' + this.code + '_cc_token').val(); + }, + /** * Set card 3DS flag * @param threedsactive diff --git a/view/adminhtml/web/js/vault.js b/view/adminhtml/web/js/vault.js index 2fd05e5..beeab71 100644 --- a/view/adminhtml/web/js/vault.js +++ b/view/adminhtml/web/js/vault.js @@ -168,6 +168,7 @@ define([ }).done(function (response) { if (response.skip_3ds) { $('body').trigger('processStop'); + self.setPaymentDetails(response.paymentIntent.id); self.placeOrder(); return; }