From 16597281d223eb776def6fd54bd7494def8c6e17 Mon Sep 17 00:00:00 2001 From: TheCoreDev Date: Tue, 1 Sep 2020 19:16:54 +0200 Subject: [PATCH] Installation du module Stripe --- composer.json | 3 +- composer.lock | 86 ++- core/vendor/composer/autoload_psr4.php | 1 + core/vendor/composer/autoload_static.php | 5 + core/vendor/composer/installed.json | 91 ++- .../Classes/StripePaymentException.php | 13 + .../Classes/StripePaymentLog.php | 60 ++ local/modules/StripePayment/Config/config.xml | 24 + local/modules/StripePayment/Config/module.xml | 24 + .../modules/StripePayment/Config/routing.xml | 28 + local/modules/StripePayment/Config/schema.xml | 5 + .../Base/StripePaymentConfigController.php | 77 +++ .../StripePaymentConfigController.php | 17 + .../Controller/StripePaymentController.php | 24 + .../Controller/StripeWebHooksController.php | 141 +++++ .../EventListeners/CartEventListener.php | 205 +++++++ .../Form/Base/StripePaymentConfigForm.php | 207 +++++++ .../Form/StripePaymentConfigForm.php | 33 ++ .../StripePayment/Hook/StripePaymentHook.php | 74 +++ .../I18n/backOffice/default/en_US.php | 12 + .../I18n/backOffice/default/fr_FR.php | 17 + .../I18n/email/default/en_US.php | 13 + .../I18n/email/default/fr_FR.php | 13 + local/modules/StripePayment/I18n/en_US.php | 21 + local/modules/StripePayment/I18n/fr_FR.php | 29 + .../I18n/frontOffice/default/fr_FR.php | 6 + local/modules/StripePayment/LICENSE.txt | 165 ++++++ .../Config/Base/StripePaymentConfigValue.php | 29 + .../Model/Config/StripePaymentConfigValue.php | 24 + local/modules/StripePayment/Readme.md | 43 ++ .../Resource/images/module/stripe.png | Bin 0 -> 58172 bytes local/modules/StripePayment/StripePayment.php | 542 ++++++++++++++++++ local/modules/StripePayment/composer.json | 12 + .../default/stripepayment-configuration.html | 197 +++++++ .../email/default/stripe_confirm_payment.html | 34 ++ .../email/default/stripe_confirm_payment.txt | 9 + .../frontOffice/default/assets/css/styles.css | 61 ++ .../js/order-invoice-after-js-include.html | 166 ++++++ .../default/assets/js/stripe-js.html | 29 + .../frontOffice/default/stripe-paiement.html | 16 + .../default2020/assets/css/styles.css | 61 ++ .../js/order-invoice-after-js-include.html | 166 ++++++ .../default2020/assets/js/stripe-js.html | 29 + .../default2020/stripe-paiement.html | 16 + 44 files changed, 2825 insertions(+), 3 deletions(-) create mode 100644 local/modules/StripePayment/Classes/StripePaymentException.php create mode 100644 local/modules/StripePayment/Classes/StripePaymentLog.php create mode 100644 local/modules/StripePayment/Config/config.xml create mode 100644 local/modules/StripePayment/Config/module.xml create mode 100644 local/modules/StripePayment/Config/routing.xml create mode 100644 local/modules/StripePayment/Config/schema.xml create mode 100644 local/modules/StripePayment/Controller/Base/StripePaymentConfigController.php create mode 100644 local/modules/StripePayment/Controller/StripePaymentConfigController.php create mode 100644 local/modules/StripePayment/Controller/StripePaymentController.php create mode 100644 local/modules/StripePayment/Controller/StripeWebHooksController.php create mode 100644 local/modules/StripePayment/EventListeners/CartEventListener.php create mode 100644 local/modules/StripePayment/Form/Base/StripePaymentConfigForm.php create mode 100644 local/modules/StripePayment/Form/StripePaymentConfigForm.php create mode 100644 local/modules/StripePayment/Hook/StripePaymentHook.php create mode 100644 local/modules/StripePayment/I18n/backOffice/default/en_US.php create mode 100644 local/modules/StripePayment/I18n/backOffice/default/fr_FR.php create mode 100644 local/modules/StripePayment/I18n/email/default/en_US.php create mode 100644 local/modules/StripePayment/I18n/email/default/fr_FR.php create mode 100644 local/modules/StripePayment/I18n/en_US.php create mode 100644 local/modules/StripePayment/I18n/fr_FR.php create mode 100644 local/modules/StripePayment/I18n/frontOffice/default/fr_FR.php create mode 100644 local/modules/StripePayment/LICENSE.txt create mode 100644 local/modules/StripePayment/Model/Config/Base/StripePaymentConfigValue.php create mode 100644 local/modules/StripePayment/Model/Config/StripePaymentConfigValue.php create mode 100644 local/modules/StripePayment/Readme.md create mode 100644 local/modules/StripePayment/Resource/images/module/stripe.png create mode 100644 local/modules/StripePayment/StripePayment.php create mode 100644 local/modules/StripePayment/composer.json create mode 100644 local/modules/StripePayment/templates/backOffice/default/stripepayment-configuration.html create mode 100644 local/modules/StripePayment/templates/email/default/stripe_confirm_payment.html create mode 100644 local/modules/StripePayment/templates/email/default/stripe_confirm_payment.txt create mode 100644 local/modules/StripePayment/templates/frontOffice/default/assets/css/styles.css create mode 100755 local/modules/StripePayment/templates/frontOffice/default/assets/js/order-invoice-after-js-include.html create mode 100755 local/modules/StripePayment/templates/frontOffice/default/assets/js/stripe-js.html create mode 100644 local/modules/StripePayment/templates/frontOffice/default/stripe-paiement.html create mode 100644 local/modules/StripePayment/templates/frontOffice/default2020/assets/css/styles.css create mode 100755 local/modules/StripePayment/templates/frontOffice/default2020/assets/js/order-invoice-after-js-include.html create mode 100755 local/modules/StripePayment/templates/frontOffice/default2020/assets/js/stripe-js.html create mode 100644 local/modules/StripePayment/templates/frontOffice/default2020/stripe-paiement.html diff --git a/composer.json b/composer.json index e29c5c2c..c5b70cd2 100644 --- a/composer.json +++ b/composer.json @@ -64,7 +64,8 @@ "commerceguys/addressing": "0.8.*", "symfony/cache": "~3.1.0", "thelia/colissimows-module": "^1.1", - "thelia/colissimo-label-module": "~0.3.2" + "thelia/colissimo-label-module": "~0.3.2", + "thelia/stripe-payment-module": "~2.0.4" }, "require-dev": { "fzaninotto/faker": "1.5.*", diff --git a/composer.lock b/composer.lock index 9e6f5c2d..2a999666 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9792ca0006636137328111bcecd57a36", + "content-hash": "5713db4d71a29f2a4aeca5a61aa6e072", "packages": [ { "name": "commerceguys/addressing", @@ -1199,6 +1199,62 @@ ], "time": "2014-11-23T20:37:11+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v6.43.1", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "42fcdaf99c44bb26937223f8eae1f263491d5ab8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/42fcdaf99c44bb26937223f8eae1f263491d5ab8", + "reference": "42fcdaf99c44bb26937223f8eae1f263491d5ab8", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "1.*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0", + "symfony/process": "~2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "time": "2019-08-29T16:56:12+00:00" + }, { "name": "swiftmailer/swiftmailer", "version": "v5.4.1", @@ -3522,6 +3578,34 @@ "description": "Number management library", "time": "2015-11-05T15:52:55+00:00" }, + { + "name": "thelia/stripe-payment-module", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/thelia-modules/StripePayment.git", + "reference": "50b758551bfee2208f2c196cc552790688f9e084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thelia-modules/StripePayment/zipball/50b758551bfee2208f2c196cc552790688f9e084", + "reference": "50b758551bfee2208f2c196cc552790688f9e084", + "shasum": "" + }, + "require": { + "stripe/stripe-php": "6.*", + "thelia/installer": "~1.1" + }, + "type": "thelia-module", + "extra": { + "installer-name": "StripePayment" + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0+" + ], + "time": "2020-06-03T13:40:26+00:00" + }, { "name": "wsdltophp/package-colissimo-postage", "version": "1.0.0", diff --git a/core/vendor/composer/autoload_psr4.php b/core/vendor/composer/autoload_psr4.php index c584decf..444d2b4d 100644 --- a/core/vendor/composer/autoload_psr4.php +++ b/core/vendor/composer/autoload_psr4.php @@ -46,6 +46,7 @@ return array( 'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'), 'Symfony\\Component\\BrowserKit\\' => array($vendorDir . '/symfony/browser-kit'), 'Symfony\\Cmf\\Component\\Routing\\' => array($vendorDir . '/symfony-cmf/routing'), + 'Stripe\\' => array($vendorDir . '/stripe/stripe-php/lib'), 'SoapClient\\' => array($vendorDir . '/wsdltophp/package-colissimo-postage/SoapClient'), 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'Giggsey\\Locale\\' => array($vendorDir . '/giggsey/locale/src'), diff --git a/core/vendor/composer/autoload_static.php b/core/vendor/composer/autoload_static.php index 753652c3..904c149f 100644 --- a/core/vendor/composer/autoload_static.php +++ b/core/vendor/composer/autoload_static.php @@ -73,6 +73,7 @@ class ComposerStaticInit60933c160e6e784f12d951b85ffd7bf5 'Symfony\\Component\\Cache\\' => 24, 'Symfony\\Component\\BrowserKit\\' => 29, 'Symfony\\Cmf\\Component\\Routing\\' => 30, + 'Stripe\\' => 7, 'SoapClient\\' => 11, ), 'P' => @@ -261,6 +262,10 @@ class ComposerStaticInit60933c160e6e784f12d951b85ffd7bf5 array ( 0 => __DIR__ . '/..' . '/symfony-cmf/routing', ), + 'Stripe\\' => + array ( + 0 => __DIR__ . '/..' . '/stripe/stripe-php/lib', + ), 'SoapClient\\' => array ( 0 => __DIR__ . '/..' . '/wsdltophp/package-colissimo-postage/SoapClient', diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json index e597bf90..aea80352 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -1296,7 +1296,8 @@ "homepage": "https://github.com/sebastianbergmann/php-token-stream/", "keywords": [ "tokenizer" - ] + ], + "abandoned": true }, { "name": "phpunit/phpunit", @@ -2227,6 +2228,64 @@ "stack" ] }, + { + "name": "stripe/stripe-php", + "version": "v6.43.1", + "version_normalized": "6.43.1.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "42fcdaf99c44bb26937223f8eae1f263491d5ab8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/42fcdaf99c44bb26937223f8eae1f263491d5ab8", + "reference": "42fcdaf99c44bb26937223f8eae1f263491d5ab8", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "1.*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0", + "symfony/process": "~2.8" + }, + "time": "2019-08-29T16:56:12+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ] + }, { "name": "swiftmailer/swiftmailer", "version": "v5.4.1", @@ -4684,6 +4743,36 @@ ], "description": "Number management library" }, + { + "name": "thelia/stripe-payment-module", + "version": "2.0.4", + "version_normalized": "2.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/thelia-modules/StripePayment.git", + "reference": "50b758551bfee2208f2c196cc552790688f9e084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thelia-modules/StripePayment/zipball/50b758551bfee2208f2c196cc552790688f9e084", + "reference": "50b758551bfee2208f2c196cc552790688f9e084", + "shasum": "" + }, + "require": { + "stripe/stripe-php": "6.*", + "thelia/installer": "~1.1" + }, + "time": "2020-06-03T13:40:26+00:00", + "type": "thelia-module", + "extra": { + "installer-name": "StripePayment" + }, + "installation-source": "dist", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0+" + ] + }, { "name": "wsdltophp/package-colissimo-postage", "version": "1.0.0", diff --git a/local/modules/StripePayment/Classes/StripePaymentException.php b/local/modules/StripePayment/Classes/StripePaymentException.php new file mode 100644 index 00000000..633ef048 --- /dev/null +++ b/local/modules/StripePayment/Classes/StripePaymentException.php @@ -0,0 +1,13 @@ + + */ +class StripePaymentException extends \Exception +{ + +} \ No newline at end of file diff --git a/local/modules/StripePayment/Classes/StripePaymentLog.php b/local/modules/StripePayment/Classes/StripePaymentLog.php new file mode 100644 index 00000000..f89d5dd0 --- /dev/null +++ b/local/modules/StripePayment/Classes/StripePaymentLog.php @@ -0,0 +1,60 @@ + + */ +class StripePaymentLog +{ + const EMERGENCY = 'EMERGENCY'; + const ALERT = 'ALERT'; + const CRITICAL = 'CRITICAL'; + const ERROR = 'ERROR'; + const WARNING = 'WARNING'; + const NOTICE = 'NOTICE'; + const INFO = 'INFO'; + const DEBUG = 'DEBUG'; + const LOGCLASS = "\\Thelia\\Log\\Destination\\TlogDestinationFile"; + + /** @var Tlog $log */ + protected $log; + + /** + * Log a message + * + * @param string $message Message + * @param string $severity EMERGENCY|ALERT|CRITICAL|ERROR|WARNING|NOTICE|INFO|DEBUG + * @param string $category Category + */ + public function logText($message, $severity = 'ALERT', $category = 'stripe') + { + $this->setTLogStripe(); + $msg = "$category.$severity: $message"; + $this->log->info($msg); + // Back to previous state + $this->getBackToPreviousState(); + } + + /** + * @return Tlog + */ + protected function setTLogStripe() + { + /* + * Write Log + */ + $this->log = Tlog::getInstance(); + $this->log->setDestinations(self::LOGCLASS); + $this->log->setConfig(self::LOGCLASS, 0, THELIA_ROOT . "log" . DS . "log-stripe.txt"); + } + + protected function getBackToPreviousState() + { + $this->log->setDestinations("\\Thelia\\Log\\Destination\\TlogDestinationRotatingFile"); + } +} \ No newline at end of file diff --git a/local/modules/StripePayment/Config/config.xml b/local/modules/StripePayment/Config/config.xml new file mode 100644 index 00000000..ff983ab9 --- /dev/null +++ b/local/modules/StripePayment/Config/config.xml @@ -0,0 +1,24 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + diff --git a/local/modules/StripePayment/Config/module.xml b/local/modules/StripePayment/Config/module.xml new file mode 100644 index 00000000..8365f823 --- /dev/null +++ b/local/modules/StripePayment/Config/module.xml @@ -0,0 +1,24 @@ + + + StripePayment\StripePayment + + Stripe + + + Stripe + + + en_US + fr_FR + + 2.0.4 + + Etienne Perriere + eperriere@openstudio.fr + + payment + 2.2.0 + other + diff --git a/local/modules/StripePayment/Config/routing.xml b/local/modules/StripePayment/Config/routing.xml new file mode 100644 index 00000000..a4fbfaba --- /dev/null +++ b/local/modules/StripePayment/Config/routing.xml @@ -0,0 +1,28 @@ + + + + StripePayment:StripePaymentConfig:default + + + StripePayment:StripePaymentConfig:save + + + StripePayment:StripeCustomer:default + + + StripePayment:StripeCustomer:create + + + StripePayment:StripeCustomer:update + + + StripePayment:StripeCustomer:processUpdate + + + StripePayment:StripeWebHooks:listen + .* + + + StripePayment:StripeCustomer:delete + + diff --git a/local/modules/StripePayment/Config/schema.xml b/local/modules/StripePayment/Config/schema.xml new file mode 100644 index 00000000..e216f239 --- /dev/null +++ b/local/modules/StripePayment/Config/schema.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/local/modules/StripePayment/Controller/Base/StripePaymentConfigController.php b/local/modules/StripePayment/Controller/Base/StripePaymentConfigController.php new file mode 100644 index 00000000..9376bcdd --- /dev/null +++ b/local/modules/StripePayment/Controller/Base/StripePaymentConfigController.php @@ -0,0 +1,77 @@ +checkAuth([AdminResources::MODULE], ["stripepayment"], AccessManager::VIEW)) { + return $response; + } + + return $this->render("stripepayment-configuration"); + } + + public function saveAction() + { + if (null !== $response = $this->checkAuth([AdminResources::MODULE], ["stripepayment"], AccessManager::UPDATE)) { + return $response; + } + + $baseForm = $this->createForm("stripepayment.configuration"); + + $errorMessage = null; + + try { + $form = $this->validateForm($baseForm); + $data = $form->getData(); + StripePayment::setConfigValue(StripePaymentConfigValue::ENABLED, is_bool($data["enabled"]) ? (int) ($data["enabled"]) : $data["enabled"]); + StripePayment::setConfigValue(StripePaymentConfigValue::STRIPE_ELEMENT, is_bool($data["stripe_element"]) ? (int) ($data["stripe_element"]) : $data["stripe_element"]); + StripePayment::setConfigValue(StripePaymentConfigValue::ONE_CLICK_PAYMENT, is_bool($data["one_click_payment"]) ? (int) ($data["one_click_payment"]) : $data["one_click_payment"]); + StripePayment::setConfigValue(StripePaymentConfigValue::SECRET_KEY, is_bool($data["secret_key"]) ? (int) ($data["secret_key"]) : $data["secret_key"]); + StripePayment::setConfigValue(StripePaymentConfigValue::PUBLISHABLE_KEY, is_bool($data["publishable_key"]) ? (int) ($data["publishable_key"]) : $data["publishable_key"]); + StripePayment::setConfigValue(StripePaymentConfigValue::WEBHOOKS_KEY, is_bool($data["webhooks_key"]) ? (int) ($data["webhooks_key"]) : $data["webhooks_key"]); + StripePayment::setConfigValue(StripePaymentConfigValue::SECURE_URL, is_bool($data["secure_url"]) ? (int) ($data["secure_url"]) : $data["secure_url"]); + } catch (FormValidationException $ex) { + // Invalid data entered + $errorMessage = $this->createStandardFormValidationErrorMessage($ex); + } catch (\Exception $ex) { + // Any other error + $errorMessage = $this->getTranslator()->trans('Sorry, an error occurred: %err', ['%err' => $ex->getMessage()], [], StripePayment::MESSAGE_DOMAIN); + } + + if (null !== $errorMessage) { + // Mark the form as with error + $baseForm->setErrorMessage($errorMessage); + + // Send the form and the error to the parser + $this->getParserContext() + ->addForm($baseForm) + ->setGeneralError($errorMessage) + ; + } else { + $this->getParserContext() + ->set("success", true) + ; + } + + return $this->defaultAction(); + } +} diff --git a/local/modules/StripePayment/Controller/StripePaymentConfigController.php b/local/modules/StripePayment/Controller/StripePaymentConfigController.php new file mode 100644 index 00000000..9e394b1c --- /dev/null +++ b/local/modules/StripePayment/Controller/StripePaymentConfigController.php @@ -0,0 +1,17 @@ + + */ +class StripePaymentController extends BasePaymentModuleController +{ + /** + * Return a module identifier used to calculate the name of the log file, + * and in the log messages. + * + * @return string the module code + */ + protected function getModuleCode() + { + return 'StripePayment'; + } +} \ No newline at end of file diff --git a/local/modules/StripePayment/Controller/StripeWebHooksController.php b/local/modules/StripePayment/Controller/StripeWebHooksController.php new file mode 100644 index 00000000..4bf5370e --- /dev/null +++ b/local/modules/StripePayment/Controller/StripeWebHooksController.php @@ -0,0 +1,141 @@ +logText(serialize($event)); + + // Handle the event + switch ($event->type) { + case 'checkout.session.completed': + /** @var Session $sessionCompleted */ + $sessionCompleted = $event->data->object; + $this->handleSessionCompleted($sessionCompleted); + break; + case 'payment_intent.succeeded': + // Needed to wait for order to be created (Stripe is faster than Thelia) + sleep(5); + /** @var Session $sessionCompleted */ + $paymentId = $event->data->object->id; + $this->handlePaymentIntentSuccess($paymentId); + break; + case 'payment_intent.payment_failed': + // Needed to wait for order to be created (Stripe is faster than Thelia) + sleep(5); + /** @var Session $sessionCompleted */ + $paymentId = $event->data->object->id; + $this->handlePaymentIntentFail($paymentId); + break; + default: + // Unexpected event type + (new StripePaymentLog())->logText('Unexpected event type'); + + return new Response('Unexpected event type', 400); + } + + return new Response('Success', 200); + } catch (\UnexpectedValueException $e) { + // Invalid payload + (new StripePaymentLog())->logText($e->getMessage()); + return new Response('Invalid payload', 400); + } catch (SignatureVerification $e) { + return new Response($e->getMessage(), 400); + } catch (\Exception $e) { + return new Response($e->getMessage(), 404); + } + } + + return new Response('Bad request', 400); + } + + protected function handleSessionCompleted(Session $sessionCompleted) + { + $order = OrderQuery::create() + ->findOneByRef($sessionCompleted->client_reference_id); + + if (null === $order) { + throw new \Exception("Order with reference $sessionCompleted->client_reference_id not found"); + } + + $this->setOrderToPaid($order); + } + + protected function handlePaymentIntentSuccess($paymentId) + { + $order = OrderQuery::create() + ->findOneByTransactionRef($paymentId); + + if (null === $order) { + throw new \Exception("Order with transaction ref $paymentId not found"); + } + + $this->setOrderToPaid($order); + } + + protected function handlePaymentIntentFail($paymentId) + { + $order = OrderQuery::create() + ->findOneByTransactionRef($paymentId); + + if (null === $order) { + throw new \Exception("Order with transaction ref $paymentId not found"); + } + + $this->setOrderToCanceled($order); + } + + protected function setOrderToPaid($order) + { + $paidStatusId = OrderStatusQuery::create() + ->filterByCode('paid') + ->select('ID') + ->findOne(); + + $event = new OrderEvent($order); + $event->setStatus($paidStatusId); + $this->getDispatcher()->dispatch(TheliaEvents::ORDER_UPDATE_STATUS, $event); + } + + protected function setOrderToCanceled($order) + { + $canceledStatusId = OrderStatusQuery::create() + ->filterByCode('canceled') + ->select('ID') + ->findOne(); + + $event = new OrderEvent($order); + $event->setStatus($canceledStatusId); + $this->getDispatcher()->dispatch(TheliaEvents::ORDER_UPDATE_STATUS, $event); + } +} diff --git a/local/modules/StripePayment/EventListeners/CartEventListener.php b/local/modules/StripePayment/EventListeners/CartEventListener.php new file mode 100644 index 00000000..cffe01b6 --- /dev/null +++ b/local/modules/StripePayment/EventListeners/CartEventListener.php @@ -0,0 +1,205 @@ +request = $request; + $this->dispatcher = $dispatcher; + $this->taxEngine = $taxEngine; + } + + public static function getSubscribedEvents() + { + $events = [ + TheliaEvents::CART_RESTORE_CURRENT => ["createOrUpdatePaymentIntent", 64], + TheliaEvents::CART_CREATE_NEW => ["createOrUpdatePaymentIntent", 64], + TheliaEvents::CART_ADDITEM => ["createOrUpdatePaymentIntent", 64], + TheliaEvents::CART_DELETEITEM => ["createOrUpdatePaymentIntent", 64], + TheliaEvents::CART_UPDATEITEM => ["createOrUpdatePaymentIntent", 64], + TheliaEvents::CART_CLEAR => ["createOrUpdatePaymentIntent", 64], + TheliaEvents::CHANGE_DEFAULT_CURRENCY => ["createOrUpdatePaymentIntent", 64], + TheliaEvents::ORDER_SET_POSTAGE => [ "createOrUpdatePaymentIntent", 64 ] + ]; + + return $events; + } + + public function createOrUpdatePaymentIntent(ActionEvent $event) + { + Stripe::setApiKey(StripePayment::getConfigValue('secret_key')); + + /** @var Session $session */ + $session = $this->request->getSession(); + + $paymentIntentValues = $this->getPaymentIntentValues($event); + + if (false === $paymentIntentValues) { + return; + } + + if ( + $session->has(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY) + && + null !== $paymentId = $session->get(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY) + ) + { + + $payment = PaymentIntent::update( + $paymentId, + $paymentIntentValues + ); + $session->set(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY, $payment->client_secret); + + return; + } + + /** @var PaymentIntent $payment */ + $payment = PaymentIntent::create($paymentIntentValues); + + $session->set(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY, $payment->id); + $session->set(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY, $payment->client_secret); + return; + } + + + protected function getPaymentIntentValues(ActionEvent $event) + { + /** @var Session $session */ + $session = $this->request->getSession(); + $currency = $session->getCurrency(); + + $data = $this->getCartAndOrderFromEvent($event); + + if (false === $data) { + return false; + } + + /** @var Cart $cart */ + $cart = $data['cart']; + + /** @var Order $order */ + $order = $data['order']; + + $postageAmount = floatval($order->getPostage()); + + $country = $this->taxEngine->getDeliveryCountry(); + + $cartAmount = floatval($cart->getTaxedAmount($country)); + + $totalAmount = ($postageAmount + $cartAmount) * 100; + + if (!$totalAmount > 0) { + return false; + } + + $values = [ + 'amount' => intval(round($totalAmount)), + 'currency' => strtolower($currency->getCode()) + ]; + + if (null !== $stripeCustomerId = $this->getStripeCustomerId($session)) { + $values['customer'] = $stripeCustomerId; + } + + return $values; + } + + protected function getStripeCustomerId(Session $session) + { + if (null === $session->getCustomerUser()) { + return null; + } + + if (!$session->has(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY)) { + /** @var Customer $customer */ + $customer = $session->getCustomerUser(); + $email = $customer->getEmail(); + + $stripeCustomer = \Stripe\Customer::create([ + 'email' => $email + ]); + + $session->set(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY, $stripeCustomer->id); + } + + return $session->get(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY); + } + + protected function getCartAndOrderFromEvent(ActionEvent $event) + { + /** @var Session $session */ + $session = $this->request->getSession(); + + if ($event instanceof CartRestoreEvent) { + return [ + 'cart' => $event->getCart(), + 'order' => $session->getOrder() + ]; + } + + if ($event instanceof CartCreateEvent) { + return [ + 'cart' => $event->getCart(), + 'order' => $session->getOrder() + ]; + } + + if ($event instanceof CartEvent) { + return [ + 'cart' => $event->getCart(), + 'order' => $session->getOrder() + ]; + } + + if ($event instanceof CurrencyChangeEvent) { + return [ + 'cart' => $session->getSessionCart($this->dispatcher), + 'order' => $session->getOrder() + ]; + } + + if ($event instanceof OrderEvent) { + return [ + 'cart' => $session->getSessionCart($this->dispatcher), + 'order' => $event->getOrder() + ]; + } + + return false; + } +} \ No newline at end of file diff --git a/local/modules/StripePayment/Form/Base/StripePaymentConfigForm.php b/local/modules/StripePayment/Form/Base/StripePaymentConfigForm.php new file mode 100644 index 00000000..036fa8d2 --- /dev/null +++ b/local/modules/StripePayment/Form/Base/StripePaymentConfigForm.php @@ -0,0 +1,207 @@ +formBuilder attribute : + * + * $this->formBuilder->add("name", "text") + * ->add("email", "email", array( + * "attr" => array( + * "class" => "field" + * ), + * "label" => "email", + * "constraints" => array( + * new \Symfony\Component\Validator\Constraints\NotBlank() + * ) + * ) + * ) + * ->add('age', 'integer'); + * + * @return null + */ + protected function buildForm() + { + $translationKeys = $this->getTranslationKeys(); + $fieldsIdKeys = $this->getFieldsIdKeys(); + + $this->addEnabledField($translationKeys, $fieldsIdKeys); + $this->addStripeElementField($translationKeys, $fieldsIdKeys); + $this->addOneClickPaymentField($translationKeys, $fieldsIdKeys); + $this->addSecretKeyField($translationKeys, $fieldsIdKeys); + $this->addPublishableKeyField($translationKeys, $fieldsIdKeys); + $this->addWebhooksKeyField($translationKeys, $fieldsIdKeys); + $this->addSecureUrlField($translationKeys, $fieldsIdKeys); + } + + protected function addEnabledField(array $translationKeys, array $fieldsIdKeys) + { + $this->formBuilder + ->add("enabled", "checkbox", array( + "label" => $this->readKey("enabled", $translationKeys), + "label_attr" => [ + "for" => $this->readKey("enabled", $fieldsIdKeys), + "help" => $this->readKey("help.enabled", $translationKeys) + ], + "required" => false, + "constraints" => array( + ), + "value" => StripePayment::getConfigValue(StripePaymentConfigValue::ENABLED, false), + )) + ; + } + + protected function addStripeElementField(array $translationKeys, array $fieldsIdKeys) + { + $this->formBuilder + ->add("stripe_element", "checkbox", array( + "label" => $this->readKey("stripe_element", $translationKeys), + "label_attr" => [ + "for" => $this->readKey("stripeelementch", $fieldsIdKeys), + "help" => $this->readKey("help.stripe_element", $translationKeys) + ], + "required" => false, + "constraints" => array( + ), + "value" => StripePayment::getConfigValue(StripePaymentConfigValue::STRIPE_ELEMENT, false), + )) + ; + } + + protected function addOneClickPaymentField(array $translationKeys, array $fieldsIdKeys) + { + $this->formBuilder + ->add("one_click_payment", "checkbox", array( + "label" => $this->readKey("one_click_payment", $translationKeys), + "label_attr" => [ + "for" => $this->readKey("one_click_payment", $fieldsIdKeys) + ], + "required" => false, + "constraints" => array( + ), + "value" => StripePayment::getConfigValue(StripePaymentConfigValue::ONE_CLICK_PAYMENT, false), + )) + ; + } + + protected function addSecretKeyField(array $translationKeys, array $fieldsIdKeys) + { + $this->formBuilder + ->add("secret_key", "text", array( + "label" => $this->readKey("secret_key", $translationKeys), + "label_attr" => [ + "for" => $this->readKey("secret_key", $fieldsIdKeys), + "help" => $this->readKey("help.secret_key", $translationKeys) + ], + "required" => true, + "constraints" => array( + new NotBlank(), + ), + "data" => StripePayment::getConfigValue(StripePaymentConfigValue::SECRET_KEY), + )) + ; + } + + protected function addPublishableKeyField(array $translationKeys, array $fieldsIdKeys) + { + $this->formBuilder + ->add("publishable_key", "text", array( + "label" => $this->readKey("publishable_key", $translationKeys), + "label_attr" => [ + "for" => $this->readKey("publishable_key", $fieldsIdKeys), + "help" => $this->readKey("help.publishable_key", $translationKeys) + ], + "required" => true, + "constraints" => array( + new NotBlank(), + ), + "data" => StripePayment::getConfigValue(StripePaymentConfigValue::PUBLISHABLE_KEY), + )) + ; + } + + protected function addWebhooksKeyField(array $translationKeys, array $fieldsIdKeys) + { + $this->formBuilder + ->add("webhooks_key", "text", array( + "label" => $this->readKey("webhooks_key", $translationKeys), + "label_attr" => [ + "for" => $this->readKey("webhooks_key", $fieldsIdKeys), + "help" => $this->readKey("help.webhooks_key", $translationKeys) + ], + "required" => true, + "constraints" => array( + new NotBlank(), + ), + "data" => StripePayment::getConfigValue(StripePaymentConfigValue::WEBHOOKS_KEY), + )) + ; + } + protected function addSecureUrlField(array $translationKeys, array $fieldsIdKeys) + { + $this->formBuilder + ->add("secure_url", "text", array( + "label" => $this->readKey("secure_url", $translationKeys), + "label_attr" => [ + "for" => $this->readKey("secure_url", $fieldsIdKeys), + "help" => $this->readKey("help.secure_url", $translationKeys) + ], + "required" => true, + "constraints" => array( + new NotBlank(), + ), + "data" => StripePayment::getConfigValue(StripePaymentConfigValue::SECURE_URL), + )) + ; + } + + public function getName() + { + return static::FORM_NAME; + } + + public function readKey($key, array $keys, $default = '') + { + if (isset($keys[$key])) { + return $keys[$key]; + } + + return $default; + } + + public function getTranslationKeys() + { + return array(); + } + + public function getFieldsIdKeys() + { + return array( + "enabled" => "enabled", + "secret_key" => "secret_key", + "publishable_key" => "publishable_key", + "webhooks_key" => "webhooks_key", + "secure_url" => "secure_url" + ); + } +} diff --git a/local/modules/StripePayment/Form/StripePaymentConfigForm.php b/local/modules/StripePayment/Form/StripePaymentConfigForm.php new file mode 100644 index 00000000..979ce2e3 --- /dev/null +++ b/local/modules/StripePayment/Form/StripePaymentConfigForm.php @@ -0,0 +1,33 @@ + $this->translator->trans("Activate payment with stripe ?", [], StripePayment::MESSAGE_DOMAIN), + "stripe_element" => $this->translator->trans("Activate Element ?", [], StripePayment::MESSAGE_DOMAIN), + "one_click_payment" => $this->translator->trans("Activate one click payment ?", [], StripePayment::MESSAGE_DOMAIN), + "secret_key" => $this->translator->trans("Your secret key", [], StripePayment::MESSAGE_DOMAIN), + "publishable_key" => $this->translator->trans("Your publishable key (test or live)", [], StripePayment::MESSAGE_DOMAIN), + "webhooks_key" => $this->translator->trans("Your webhooks key", [], StripePayment::MESSAGE_DOMAIN), + "secure_url" => $this->translator->trans("Your chain of char for secure return webhook", [], StripePayment::MESSAGE_DOMAIN), + "help.enabled" => $this->translator->trans("Do you want to activate Stripe Payment", [], StripePayment::MESSAGE_DOMAIN), + "help.stripe_element" => $this->translator->trans("Element is the embedded and customizable payment form", [], StripePayment::MESSAGE_DOMAIN), + "help.secret_key" => $this->translator->trans("You can see all your keys in your Stripe dashboard. Also note that you can place your test or your live API keys", [], StripePayment::MESSAGE_DOMAIN), + ); + } +} diff --git a/local/modules/StripePayment/Hook/StripePaymentHook.php b/local/modules/StripePayment/Hook/StripePaymentHook.php new file mode 100644 index 00000000..f82f2654 --- /dev/null +++ b/local/modules/StripePayment/Hook/StripePaymentHook.php @@ -0,0 +1,74 @@ + + */ +class StripePaymentHook extends BaseHook +{ + protected $request; + + protected $taxEngine; + + public function __construct(Request $request, TaxEngine $taxEngine) + { + $this->request = $request; + $this->taxEngine = $taxEngine; + } + + public function includeStripe(HookRenderEvent $event) + { + if(StripePayment::getConfigValue('stripe_element')){ + $publicKey = StripePayment::getConfigValue('publishable_key'); + $clientSecret = $this->request->getSession()->get(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY); + $currency = strtolower($this->request->getSession()->getCurrency()->getCode()); + $country = $this->taxEngine->getDeliveryCountry()->getIsoalpha2(); + $event->add($this->render( + 'assets/js/stripe-js.html', + [ + 'stripe_module_id' => $this->getModule()->getModuleId(), + 'public_key' => $publicKey, + 'oneClickPayment' => StripePayment::getConfigValue(StripePaymentConfigValue::ONE_CLICK_PAYMENT, false), + 'clientSecret' => $clientSecret, + 'currency' => $currency, + 'country' => $country + ] + )); + } + } + + public function declareStripeOnClickEvent(HookRenderEvent $event) + { + if(StripePayment::getConfigValue('stripe_element')){ + $publicKey = StripePayment::getConfigValue('publishable_key'); + $event->add($this->render( + 'assets/js/order-invoice-after-js-include.html', + [ + 'stripe_module_id' => $this->getModule()->getModuleId(), + 'public_key' => $publicKey + ] + )); + } + } + + public function includeStripeJsV3(HookRenderEvent $event) + { + $event->add(''); + } + + public function onMainHeadBottom(HookRenderEvent $event) + { + $content = $this->addCSS('assets/css/styles.css'); + $event->add($content); + } +} \ No newline at end of file diff --git a/local/modules/StripePayment/I18n/backOffice/default/en_US.php b/local/modules/StripePayment/I18n/backOffice/default/en_US.php new file mode 100644 index 00000000..115c80f5 --- /dev/null +++ b/local/modules/StripePayment/I18n/backOffice/default/en_US.php @@ -0,0 +1,12 @@ + 'Configuration correctly saved', + 'Configure stripepayment' => 'Configure stripepayment', + 'Home' => 'Home', + 'Modules' => 'Modules', + 'StripePayment configuration' => 'StripePayment configuration', + 'The configuration value enabled' => 'The configuration value enabled', + 'The configuration value publishable_key' => 'The configuration value publishable_key', + 'The configuration value secret_key' => 'The configuration value secret_key', +); diff --git a/local/modules/StripePayment/I18n/backOffice/default/fr_FR.php b/local/modules/StripePayment/I18n/backOffice/default/fr_FR.php new file mode 100644 index 00000000..7fc731d3 --- /dev/null +++ b/local/modules/StripePayment/I18n/backOffice/default/fr_FR.php @@ -0,0 +1,17 @@ + 'Configuration correctement sauvegardée', + 'Configure stripepayment' => 'Configurer Stripe', + 'Home' => 'Accueil', + 'Modules' => 'Modules', + 'StripePayment configuration' => 'Configuration du module Stripe', + 'The configuration value enabled' => 'La valeur de configuration "enabled"', + 'The configuration value publishable_key' => 'La valeur de configuration "publishable_key"', + 'The configuration value secret_key' => 'La valeur de configuration "secret_key"', + 'The configuration value secure_url' => 'La valeur de configuration secure_url', + 'The configuration value webhooks_key whsec_...' => 'La valeur de configuration webhooks_key whsec_...', + 'Webhooks endpoint url' => 'URL d\'endpoint WebHook', + 'Webhooks event to activate' => 'Événements WebHook à activer', + 'active stripe element' => 'Activer Stripe Element', +); diff --git a/local/modules/StripePayment/I18n/email/default/en_US.php b/local/modules/StripePayment/I18n/email/default/en_US.php new file mode 100644 index 00000000..a3bd1cb6 --- /dev/null +++ b/local/modules/StripePayment/I18n/email/default/en_US.php @@ -0,0 +1,13 @@ + 'Dear customer,', + 'Payment is confirmed for your order' => 'Payment is confirmed for your order', + 'Reference %ref' => 'Reference: %ref', + 'Thank you again for your purchase.' => 'Thank you again for your purchase!', + 'Thank you for your order!' => 'Thank you for your order!', + 'The %name team.' => 'The %name team.', + 'This is a confirmation of the payment of your order %order on %name.' => 'This is a confirmation of the payment of your order %order on %name.', + 'Your invoice is now available in your customer account on' => 'Your invoice is now available in your customer account on', + 'Your invoice is now available in your customer account on %site' => 'Your invoice is now available in your customer account on %site.', +); diff --git a/local/modules/StripePayment/I18n/email/default/fr_FR.php b/local/modules/StripePayment/I18n/email/default/fr_FR.php new file mode 100644 index 00000000..2b922e51 --- /dev/null +++ b/local/modules/StripePayment/I18n/email/default/fr_FR.php @@ -0,0 +1,13 @@ + 'Cher client,', + 'Payment is confirmed for your order' => 'Le paiement de votre commande est confirmé', + 'Reference %ref' => 'Référence de commande : %ref', + 'Thank you again for your purchase.' => 'Merci encore pour cet achat !', + 'Thank you for your order!' => 'Merci pour votre commande !', + 'The %name team.' => 'L\'équipe %name.', + 'This is a confirmation of the payment of your order %order on %name.' => 'Ce message confirme le paiement de votre commande n° %order sur %name.', + 'Your invoice is now available in your customer account on' => 'Votre facture est maintenant disponible sur votre compte sur ', + 'Your invoice is now available in your customer account on %site' => 'Votre facture est maintenant disponible sur votre compte sur %site. ', +); diff --git a/local/modules/StripePayment/I18n/en_US.php b/local/modules/StripePayment/I18n/en_US.php new file mode 100644 index 00000000..88eb9c9d --- /dev/null +++ b/local/modules/StripePayment/I18n/en_US.php @@ -0,0 +1,21 @@ + 'Activated ?', + 'An error occurred during payment.' => 'An error occurred during payment.', + 'An error occurred with Stripe.' => 'An error occurred with Stripe.', + 'Authentication with Stripe failed. Please contact administrators.' => 'Authentication with Stripe failed. Please contact administrators.', + 'Do you want to activate Stripe Payment' => 'Do you want to activate Stripe Payment', + 'Invalid parameters were supplied to Stripe.' => 'Invalid parameters were supplied to Stripe.', + 'Network communication failed.' => 'Network communication failed.', + 'Payment confirmation of your order {$order_ref} on %store_name' => 'Payment confirmation of your order {$order_ref} on %store_name', + 'Payment confirmation on %store_name' => 'Payment confirmation on %store_name', + 'Sorry, an error occurred: %err' => 'Sorry, an error occurred: %err', + 'Stripe library isn\'t installed' => 'Stripe library isn\'t installed', + 'The payment mean does not have the same amount as your cart. Please reload and try again.' => 'The payment mean does not have the same amount as your cart. Please reload and try again.', + 'Too many requests too quickly.' => 'Too many requests too quickly.', + 'You can see all your keys in your Stripe dashboard. Also note that you can place your test or your live API keys' => 'You can see all your keys in your Stripe dashboard. Also note that you have to place your test and your live API keys', + 'Your card has been declined.' => 'Your card has been declined.', + 'Your publishable key (test or live)' => 'Your publishable key (test or live)', + 'Your secret key' => 'Your secret key', +); diff --git a/local/modules/StripePayment/I18n/fr_FR.php b/local/modules/StripePayment/I18n/fr_FR.php new file mode 100644 index 00000000..c2655677 --- /dev/null +++ b/local/modules/StripePayment/I18n/fr_FR.php @@ -0,0 +1,29 @@ + 'Activer Element ?', + 'Activate payment with stripe ?' => 'Activer le paiement avec Stripe ?', + 'An error occurred during payment.' => 'Une erreur est survenue lors du paiement.', + 'An error occurred with Stripe.' => 'Une erreur est survenue lors du paiement avec Stripe.', + 'Authentication with Stripe failed. Please contact administrators.' => 'Des paramètres invalides ont été envoyés à Stripe.', + 'Discount' => 'Remise', + 'Do you want to activate Stripe Payment' => 'Voulez-vous activer Stripe ?', + 'Element is the embedded and customizable payment form' => 'Element est un formulaire intégrer et personlisable.', + 'Invalid parameters were supplied to Stripe.' => 'Une erreur liée au réseau empêche la communication avec Stripe.', + 'Network communication failed.' => 'L\'authentification auprès de Stripe a échoué. Merci de contacter les administrateurs du site.', + 'Payment confirmation for Stripe Payment' => 'Confirmation de paiement par Stripe', + 'Payment confirmation of your order {$order_ref} on {$store_name}' => 'Confirmation de paiement pour votre commande {$order_ref} sur {$store_name}', + 'Sorry, an error occurred: %err' => 'Désolé, une erreur est survenue : %err', + 'Stripe library is missing.' => 'La libraire Stripe est manquante', + 'Stripe version is greater than max version (< %version). Current version: %curVersion.' => 'La version de la libraire Stripe est plus haute qua la version maximum ( < %version). Version actuelle %curVersion.', + 'Stripe version is lower than min version (%version). Current version: %curVersion.' => 'La version de la libraire Stripe est plus basse qua la version minimum ( > %version). Version actuelle %curVersion.', + 'The payment mean does not have the same amount as your cart. Please reload and try again.' => 'Le moyen de paiement n\'indique pas le même montant que votre panier. Rechargez la pager et réessayez.', + 'Too many requests too quickly.' => 'Trop de requêtes en peu de temps.', + 'Total' => 'Total', + 'You can see all your keys in your Stripe dashboard. Also note that you can place your test or your live API keys' => 'Vos clés sont disponibles dans votre compte sur la plateforme d\'administration Stripe. Veuillez noter que vous devez renseigner ici vos clés publique et privée.', + 'Your card has been declined.' => 'Votre carte bancaire a été refusée.', + 'Your chain of char for secure return webhook' => 'Une chaine de caractère pour sécuriser le retour des Webhooks', + 'Your publishable key (test or live)' => 'Votre clé publique', + 'Your secret key' => 'Votre clé secrète', + 'Your webhooks key' => 'Votre clé Webhooks ?', +); diff --git a/local/modules/StripePayment/I18n/frontOffice/default/fr_FR.php b/local/modules/StripePayment/I18n/frontOffice/default/fr_FR.php new file mode 100644 index 00000000..855d244d --- /dev/null +++ b/local/modules/StripePayment/I18n/frontOffice/default/fr_FR.php @@ -0,0 +1,6 @@ + 'Ou entrer les détails de votre carte de crédit', + 'Quick pay' => 'Paiement rapide', +); diff --git a/local/modules/StripePayment/LICENSE.txt b/local/modules/StripePayment/LICENSE.txt new file mode 100644 index 00000000..65c5ca88 --- /dev/null +++ b/local/modules/StripePayment/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/local/modules/StripePayment/Model/Config/Base/StripePaymentConfigValue.php b/local/modules/StripePayment/Model/Config/Base/StripePaymentConfigValue.php new file mode 100644 index 00000000..7b497a20 --- /dev/null +++ b/local/modules/StripePayment/Model/Config/Base/StripePaymentConfigValue.php @@ -0,0 +1,29 @@ +/local/modules/``` directory and be sure that the name of the module is StripePayment. +* Install the Stripe PHP library : + * add "stripe/stripe-php" to your composer.json file with command : `composer require stripe/stripe-php:"6.*"` + * or download the library from and install it in your `core/vendor` directory +* Activate it in your Thelia administration panel + + +### Composer + +Add it in your main thelia composer.json file: + +``` +composer require thelia/stripe-payment-module ~2.0.0 +``` + +### Configuration + +Enter your Stripe keys (*secret* and *public*) available on your [Stripe dashboard](https://dashboard.stripe.com/). + +Put your Stripe account in live mode. + +Then activate the Stripe in the module configuration panel. + +Activate the webhooks in stripe dashboard with the url specified in Thelia Back-office Stripe configuration, +and add events listed in Thelia Back-office Stripe configuration. + +### Logs + +Stripe error logs are stored in a specific file located in the log folder. diff --git a/local/modules/StripePayment/Resource/images/module/stripe.png b/local/modules/StripePayment/Resource/images/module/stripe.png new file mode 100644 index 0000000000000000000000000000000000000000..20b7de7b5736fee12a83cbb932e87b38127c2fb2 GIT binary patch literal 58172 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

158iNo&|W@WJ46i$la-Km^hsBM)EiW4 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 + */ +class StripePayment extends AbstractPaymentModule +{ + const MESSAGE_DOMAIN = "stripepayment"; + const CONFIRMATION_MESSAGE_NAME = "stripe_confirm_payment"; + const STRIPE_VERSION_MIN = "3.0.0"; + const STRIPE_VERSION_MAX = "7.0.0"; + + const PAYMENT_INTENT_ID_SESSION_KEY = 'payment_intent_id'; + const PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY = 'payment_intent_customer_id'; + const PAYMENT_INTENT_SECRET_SESSION_KEY = 'payment_intent_secret'; + + public function preActivation(ConnectionInterface $con = null) + { + // Check if Stripe API is present + try { + $this->checkApi(); + } catch (\Exception $ex) { + throw $ex; + } + + return true; + } + + public function postActivation(ConnectionInterface $con = null) + { + // Module image + $moduleModel = $this->getModuleModel(); + + if (! $moduleModel->isModuleImageDeployed($con)) { + $this->deployImageFolder($moduleModel, sprintf('%s'.DS.'Resource'.DS.'images'.DS.'module', __DIR__), $con); + } + + $this->createMailMessage(); + } + + public function createMailMessage() + { + // Create payment confirmation message from templates, if not already defined + if (null === MessageQuery::create()->findOneByName(self::CONFIRMATION_MESSAGE_NAME)) { + + $languages = LangQuery::create()->find(); + + $message = new Message(); + $message + ->setName(self::CONFIRMATION_MESSAGE_NAME) + ->setHtmlTemplateFileName(self::CONFIRMATION_MESSAGE_NAME.'.html') + ->setTextTemplateFileName(self::CONFIRMATION_MESSAGE_NAME.'.txt') + ; + + foreach ($languages as $language) { + /** @var Lang $language */ + $locale = $language->getLocale(); + $message + ->setLocale($locale) + ->setTitle( + Translator::getInstance()->trans( + "Payment confirmation for Stripe Payment", + [], + self::MESSAGE_DOMAIN, + $locale + ) + ) + ->setSubject( + Translator::getInstance()->trans( + 'Payment confirmation of your order {$order_ref} on {$store_name}', + [], + self::MESSAGE_DOMAIN, + $locale + ) + ) + ; + } + + $message->save(); + } + } + + public function checkApi() + { + try { + $ReflectedClass = new \ReflectionClass('Stripe\Stripe'); + } catch (\Exception $ex) { + throw new \Exception( + Translator::getInstance()->trans( + "Stripe library is missing.", + [], + self::MESSAGE_DOMAIN + ) + ); + } + + $stripeVersion = \Stripe\Stripe::VERSION; + + if (version_compare(self::STRIPE_VERSION_MIN, $stripeVersion) == 1) { + throw new \Exception( + Translator::getInstance()->trans( + "Stripe version is lower than min version (%version). Current version: %curVersion.", + [ + '%version' => self::STRIPE_VERSION_MIN, + '%curVersion' => $stripeVersion + ], + self::MESSAGE_DOMAIN + ) + ); + } + + if (version_compare(self::STRIPE_VERSION_MAX, $stripeVersion) < 1) { + throw new \Exception( + Translator::getInstance()->trans( + "Stripe version is greater than max version (< %version). Current version: %curVersion.", + [ + '%version' => self::STRIPE_VERSION_MAX, + '%curVersion' => $stripeVersion + ], + self::MESSAGE_DOMAIN + ) + ); + } + } + + /** + * + * Method used by payment gateway. + * + * If this method return a \Thelia\Core\HttpFoundation\Response instance, this response is send to the + * browser. + * + * In many cases, it's necessary to send a form to the payment gateway. On your response you can return this form already + * completed, ready to be sent + * + * @param \Thelia\Model\Order $order processed order + * @return null|\Thelia\Core\HttpFoundation\Response + */ + public function pay(Order $order) + { + if (!$this->isValidPayment()) { + throw new Exception("Your connection is not secured. Check that 'https' is present at the beginning of the site's address."); + } + + return $this->doPay($order); + } + + protected function doPay(Order $order) + { + Stripe::setApiKey(StripePayment::getConfigValue('secret_key')); + $session = $this->getRequest()->getSession(); + + try { + + if(StripePayment::getConfigValue('stripe_element')){ + $order->setTransactionRef($session->get(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY)) + ->save(); + $session->set(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY, null); + $session->set(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY, null); + $session->set(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY, null); + + return; + }else{ + $session->set(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY, null); + $session->set(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY, null); + $session->set(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY, null); + + // Create the session on Stripe's servers - this will charge the user's order and save session id into order transaction reference + return $this->createStripeSession($order); + } + + } catch(\Stripe\Error\Card $e) { + // The card has been declined + // FIXME Translate message here + $logMessage = sprintf( + 'Error paying order %d with Stripe. Card declined. Message: %s', + $order->getId(), + $e->getMessage() + ); + + $userMessage = Translator::getInstance() + ->trans( + 'Your card has been declined.', + [], + StripePayment::MESSAGE_DOMAIN + ); + } catch (\Stripe\Error\RateLimit $e) { + // Too many requests made to the API too quickly + $logMessage = sprintf( + 'Error paying order %d with Stripe. Too many requests. Message: %s', + $order->getId(), + $e->getMessage() + ); + + $userMessage = Translator::getInstance() + ->trans( + 'Too many requests too quickly.', + [], + StripePayment::MESSAGE_DOMAIN + ); + } catch (\Stripe\Error\InvalidRequest $e) { + // Invalid parameters were supplied to Stripe's API + $logMessage = sprintf( + 'Error paying order %d with Stripe. Invalid parameters. Message: %s', + $order->getId(), + $e->getMessage() + ); + + $userMessage = Translator::getInstance() + ->trans( + 'Invalid parameters were supplied to Stripe.', + [], + StripePayment::MESSAGE_DOMAIN + ); + } catch (\Stripe\Error\Authentication $e) { + // Authentication with Stripe's API failed + // (maybe you changed API keys recently) + $logMessage = sprintf( + 'Error paying order %d with Stripe. Authentication failed: API key changed? Message: %s', + $order->getId(), + $e->getMessage() + ); + + $userMessage = Translator::getInstance() + ->trans( + 'Authentication with Stripe failed. Please contact administrators.', + [], + StripePayment::MESSAGE_DOMAIN + ); + } catch (\Stripe\Error\ApiConnection $e) { + // Network communication with Stripe failed + $logMessage = sprintf( + 'Error paying order %d with Stripe. Network communication failed. Message: %s', + $order->getId(), + $e->getMessage() + ); + + $userMessage = Translator::getInstance() + ->trans( + 'Network communication failed.', + [], + StripePayment::MESSAGE_DOMAIN + ); + } catch (\Stripe\Error\Base $e) { + // Display a very generic error to the user + $logMessage = sprintf( + 'Error paying order %d with Stripe. Message: %s', + $order->getId(), + $e->getMessage() + ); + + $userMessage = Translator::getInstance() + ->trans( + 'An error occurred with Stripe.', + [], + StripePayment::MESSAGE_DOMAIN + ); + } catch (StripePaymentException $e) { + // Amount shown to the user by Stripe & order amount are not equal + $logMessage = sprintf( + 'Error paying order %d with Stripe. Amounts are different. Message: %s', + $order->getId(), + $e->getMessage() + ); + + $userMessage = $e->getMessage(); + } catch (\Exception $e) { + // Something else happened, completely unrelated to Stripe + $logMessage = sprintf( + 'Error paying order %d with Stripe but maybe unrelated with it. Message: %s', + $order->getId(), + $e->getMessage() + ); + + $userMessage = Translator::getInstance() + ->trans( + 'An error occurred during payment.', + [], + StripePayment::MESSAGE_DOMAIN + ); + } + + if ($logMessage !== NULL) { + (new StripePaymentLog())->logText($logMessage); + + return new RedirectResponse( + URL::getInstance()->absoluteUrl("/order/failed/".$order->getId()."/".$userMessage) + ); + } + + return new Response(); + } + + public function createStripeSession(OrderModel $order) + { + /* Impossible d'ajouter une ligne spécifique pour la remise, cette partie est mise de côté en attendant que stripe ajoute cette possibilité + + $lineItems = $this->prepareLineItems($order); + + */ + + $currency = $order->getCurrency(); + + if (null === $currency) { + $currency = $this->getRequest()->getSession()->getCurrency(); + } + + $lineItems[] = [ + 'name'=> Translator::getInstance()->trans('Total', [], StripePayment::MESSAGE_DOMAIN ), + 'description' => null, + 'quantity'=> 1, + 'currency' => strtolower($currency->getCode()), + 'amount' => round($order->getTotalAmount(), 2) * 100 + ]; + + if(empty($lineItems)){ + throw new \Exception("Sorry, your cart is empty. There's nothing to pay."); + } + + $session = Session::create([ + 'customer_email' => $order->getCustomer()->getEmail(), + 'client_reference_id' => $order->getRef(), + 'payment_method_types' => ['card'], + 'line_items' => $lineItems, + 'success_url' => URL::getInstance()->absoluteUrl('/order/placed/' . $order->getId()), + 'cancel_url' => URL::getInstance()->absoluteUrl('/order/failed/' . $order->getId() . '/error'), + ]); + + $order->setTransactionRef($session->payment_intent)->save(); + + /** @var ParserInterface $parser */ + $parser = $this->getContainer()->get("thelia.parser"); + + $parser->setTemplateDefinition( + $parser->getTemplateHelper()->getActiveFrontTemplate() + ); + + $renderedTemplate = $parser->render( + "stripe-paiement.html", + [ + 'checkout_session_id' => $session->id, + 'public_key' => StripePayment::getConfigValue('publishable_key') + ] + ); + + return Response::create($renderedTemplate); + } + + /** + * + * This method is call on Payment loop. + * + * If you return true, the payment method will be display + * If you return false, the payment method will not be display + * + * @return boolean + */ + public function isValidPayment() + { + return ( ($this->isDevEnvironment() || $this->isSslEnabled()) && $this->getConfigValue('enabled') ); + } + + /** + * Return true if the current environment is in Dev mode + * + * @return bool + */ + protected function isDevEnvironment() + { + return 'dev' == $this->getContainer()->getParameter('kernel.environment'); + } + + /** + * return true if SSL is enabled + * + * @return bool + */ + protected function isSslEnabled() + { + return $this->getRequest()->isSecure(); + } + + public function checkOrderAmount(OrderModel $order, $stripeAmount) + { + $orderAmount = $order->getTotalAmount() * 100; + + if (strval($stripeAmount) != strval($orderAmount)) { + throw new StripePaymentException(Translator::getInstance() + ->trans( + 'The payment mean does not have the same amount as your cart. Please reload and try again.', + [], + StripePayment::MESSAGE_DOMAIN + ) + ); + } + } + + protected function prepareLineItems(Order $order, $currency) + { + $stripeAmount = 0; + $lineItems = []; + + $baseSourceFilePath = ConfigQuery::read('images_library_path'); + if ($baseSourceFilePath === null) { + $baseSourceFilePath = THELIA_LOCAL_DIR . 'media' . DS . 'images'; + } else { + $baseSourceFilePath = THELIA_ROOT . $baseSourceFilePath; + } + if(null !== $orderProducts = OrderProductQuery::create()->filterByOrderId($order->getId())->joinOrderProductTax('opt', Criteria::LEFT_JOIN)->withColumn('SUM(`opt`.AMOUNT)', 'TOTAL_TAX')->withColumn('SUM(`opt`.PROMO_AMOUNT)', 'TOTAL_PROMO_TAX')->groupById()->find()){ + foreach ($orderProducts as $orderProduct) { + $description=''; + if(null !== $orderProductAttributeCombinations = OrderProductAttributeCombinationQuery::create()->filterByOrderProductId($orderProduct->getId())->find()){ + foreach ($orderProductAttributeCombinations as $orderProductAttributeCombination) { + if($description) $description .= ', '; + $description .= $orderProductAttributeCombination->getAttributeTitle() . ' ' . $orderProductAttributeCombination->getAttributeAvTitle(); + } + } + $images=array(); + if(null !== $product = ProductQuery::create()->filterByRef($orderProduct->getProductRef())->findOne()){ + if(null !== $productImages = ProductImageQuery::create()->filterByProductId($product->getId())->filterByVisible(1)->orderBy('position')->find()){ + foreach ($productImages as $productImage) { + // Put source image file path + $sourceFilePath = sprintf( + '%s/%s/%s', + $baseSourceFilePath, + 'product', + $productImage->getFile() + ); + + // Create image processing event + $event = new ImageEvent(); + $event->setSourceFilepath($sourceFilePath); + $event->setCacheSubdirectory('product'); + $width=100; + try { + // Dispatch image processing event + $event->setWidth($width); + $order->getDispatcher()->dispatch(TheliaEvents::IMAGE_PROCESS, $event); + $images[]=$event->getFileUrl(); + } catch (\Exception $ex) { + // Ignore the result and log an error + Tlog::getInstance()->addError(sprintf("Failed to process image in image loop: %s", $ex->getMessage())); + } + } + } + } + if($orderProduct->getWasInPromo()){ + $amount = $orderProduct->getPromoPrice() + $orderProduct->getVirtualColumn('TOTAL_PROMO_TAX'); + }else{ + $amount = $orderProduct->getPrice() + $orderProduct->getVirtualColumn('TOTAL_TAX'); + } + + $stripeAmount += $amount * $orderProduct->getQuantity() * 100; + $lineItems[] = [ + 'name' => $orderProduct->getTitle(), + 'description' => $description, + 'images' => $images, + 'amount' => $amount*100, + 'currency' => $currency, + 'quantity' => $orderProduct->getQuantity(), + ]; + } + } + if ($order->getPostage()){ + if (null !== $module = ModuleQuery::create()->findPk($order->getDeliveryModuleId())){ + $locale = $this->getRequest()->getLocale(); + if ($locale == 'en') { + $locale = 'en_US'; + } + $module->setLocale($locale); + + if (!$module->getTitle()) { + $module->setLocale('fr_FR'); + } + $lineItems[] = ['name'=> $module->getTitle(), 'description' => $module->getChapo(), 'quantity'=> 1, 'currency' => $currency, 'amount' => ($order->getPostage()*100)]; + $stripeAmount += $order->getPostage() * 100; + } + } + + if($order->getDiscount() > 0){ + $description=null; + if(null !== $orderCoupons = OrderCouponQuery::create()->filterByOrderId($order->getId())->find()){ + foreach($orderCoupons as $orderCoupon){ + if($description)$description .= ', '; + $description .= $orderCoupon->getTitle(); + } + } + $lineItems[] = ['name'=> Translator::getInstance()->trans('Discount', [], StripePayment::MESSAGE_DOMAIN ), 'description' => $description, 'quantity'=> 1, 'currency' => $currency, 'amount' => -($order->getDiscount()*100)]; + $stripeAmount -= $order->getDiscount() * 100; + } + + $this->checkOrderAmount($order, $stripeAmount); + + return $lineItems; + } + + + /** + * if you want, you can manage stock in your module instead of order process. + * Return false to decrease the stock when order status switch to pay + * + * @return bool + */ + public function manageStockOnCreation() + { + return false; + } +} diff --git a/local/modules/StripePayment/composer.json b/local/modules/StripePayment/composer.json new file mode 100644 index 00000000..6cf93cea --- /dev/null +++ b/local/modules/StripePayment/composer.json @@ -0,0 +1,12 @@ +{ + "name": "thelia/stripe-payment-module", + "license": "LGPL-3.0+", + "type": "thelia-module", + "require": { + "thelia/installer": "~1.1", + "stripe/stripe-php": "6.*" + }, + "extra": { + "installer-name": "StripePayment" + } +} \ No newline at end of file diff --git a/local/modules/StripePayment/templates/backOffice/default/stripepayment-configuration.html b/local/modules/StripePayment/templates/backOffice/default/stripepayment-configuration.html new file mode 100644 index 00000000..a9b7a536 --- /dev/null +++ b/local/modules/StripePayment/templates/backOffice/default/stripepayment-configuration.html @@ -0,0 +1,197 @@ +{extends file="admin-layout.tpl"} + +{block name="no-return-functions"} + {$admin_current_location = 'modules'} +{/block} + +{block name="page-title"}{intl d="stripepayment.bo.default" l='StripePayment configuration'}{/block} + +{block name="check-resource"}admin.module{/block} +{block name="check-access"}view{/block} +{block name="check-module"}StripePayment{/block} + +{block name="main-content"} +

+ + +
+
+ {intl l="Configure stripepayment" d="stripepayment.bo.default"} +
+ +
+
+ {if $success} +
+ {intl l="Configuration correctly saved" d="stripepayment.bo.default"} +
+ {/if} + + {form name="stripepayment.configuration"} + + {include "includes/inner-form-toolbar.html" hide_flags = 1 close_url={url path='/admin/modules'}} +
+ + {form_field form=$form field="success_url"} + + {/form_field} + + {form_hidden_fields form=$form} + + {form_field form=$form field="enabled"} +
+ + + {if ! empty($label_attr.help)} + {$label_attr.help} + {/if} +
+ {/form_field} + {form_field form=$form field="stripe_element"} +
+ + + {if ! empty($label_attr.help)} + {$label_attr.help} + {/if} +
+ {/form_field} + {form_field form=$form field="one_click_payment"} +
+ + + {if ! empty($label_attr.help)} + {$label_attr.help} + {/if} +
+ {/form_field} + {form_field form=$form field="secret_key"} +
+ + + + {if ! empty($label_attr.help)} + {$label_attr.help nofilter} + {/if} +
+ {/form_field} + {form_field form=$form field="publishable_key"} +
+ + + + {if ! empty($label_attr.help)} + {$label_attr.help} + {/if} +
+ {/form_field} + + {form_field form=$form field="webhooks_key"} +
+ + + + {if ! empty($label_attr.help)} + {$label_attr.help} + {/if} +
+ {/form_field} + + {form_field form=$form field="secure_url"} +
+ + + + {if ! empty($label_attr.help)} + {$label_attr.help} + {/if} +
+ {/form_field} + +
+ + +
+ +
+ +
    +
  • payment_intent.payment_failed
  • +
  • payment_intent.succeeded
  • +
  • checkout.session.completed
  • +
+
+ {include "includes/inner-form-toolbar.html" hide_flags = 1 close_url={url path='/admin/modules'} page_bottom=1} + + {/form} +
+
+
+
+{/block} + +{block name="javascript-initialization"} +{/block} diff --git a/local/modules/StripePayment/templates/email/default/stripe_confirm_payment.html b/local/modules/StripePayment/templates/email/default/stripe_confirm_payment.html new file mode 100644 index 00000000..b57d7550 --- /dev/null +++ b/local/modules/StripePayment/templates/email/default/stripe_confirm_payment.html @@ -0,0 +1,34 @@ +{extends file="email-layout.tpl"} + +{* Open in browser *} +{block name="browser"}{/block} + +{* No big image header *} +{block name="image-header"}{/block} + +{* No pre-header *} +{block name="pre-header"}{/block} + +{* Subject *} +{block name="email-subject"}Payment confirmation {$store_name}{/block} + +{* Title *} +{block name="email-title"}{/block} + +{* Content *} +{block name="email-content"} +

{$store_name}

+ +

{intl l="Payment is confirmed for your order" d="stripepayment.email.default"}

+ +

{intl l="Reference %ref" ref={$order_ref} d="stripepayment.email.default"}

+ +

+ {intl l="Your invoice is now available in your customer account on" d="stripepayment.email.default"} + {$store_name}. +

+ +

{intl l="Thank you for your order!" d="stripepayment.email.default"}

+ +

{intl l="The %name team." name={$store_name} d="stripepayment.email.default"}

+{/block} diff --git a/local/modules/StripePayment/templates/email/default/stripe_confirm_payment.txt b/local/modules/StripePayment/templates/email/default/stripe_confirm_payment.txt new file mode 100644 index 00000000..1e3c8660 --- /dev/null +++ b/local/modules/StripePayment/templates/email/default/stripe_confirm_payment.txt @@ -0,0 +1,9 @@ +{intl l="Dear customer," d="stripepayment.email.default"} + +{intl l="This is a confirmation of the payment of your order %order on %name." order={$order_ref} name={$store_name} d="stripepayment.email.default"} + +{intl l="Your invoice is now available in your customer account on %site" site={$store_url} d="stripepayment.email.default"} + +{intl l="Thank you again for your purchase." d="stripepayment.email.default"} + +{intl l="The %name team." name={$store_name} d="stripepayment.email.default"} diff --git a/local/modules/StripePayment/templates/frontOffice/default/assets/css/styles.css b/local/modules/StripePayment/templates/frontOffice/default/assets/css/styles.css new file mode 100644 index 00000000..87bff2d1 --- /dev/null +++ b/local/modules/StripePayment/templates/frontOffice/default/assets/css/styles.css @@ -0,0 +1,61 @@ +/** + * The CSS shown here will not be introduced in the Quickstart guide, but shows + * how you can use CSS to style your Element's container. + */ +.payment { + margin-bottom: 20px; +} +.stripe-payment { + width: 80%; + margin: auto; + text-align: center; +} +.stripe-payment .payment{ + background-color: #f5f5f5; + border-radius: 5px; +} +.stripe-payment .payment-label { + font-size: 20px; + font-weight: 500; +} +#payment-request-button { + margin-top: 10px; +} +#card-element { + box-sizing: border-box; + + height: 40px; + + padding: 10px 12px; + + border: 1px solid transparent; + border-radius: 4px; + background-color: white; + + box-shadow: 0 1px 3px 0 #e6ebf1; + -webkit-transition: box-shadow 150ms ease; + transition: box-shadow 150ms ease; +} + +#card-element--focus { + box-shadow: 0 1px 3px 0 #cfd7df; +} + +#card-element--invalid { + border-color: #fa755a; +} + +#card-element--webkit-autofill { + background-color: #fefde5 !important; +} +#card-errors, #payment-request-errors { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; + border-radius: .25rem; + padding: .75rem 1.25rem; + text-align: center; +} +#card-errors.hidden, #payment-request-errors.hidden { + display: none; +} \ No newline at end of file diff --git a/local/modules/StripePayment/templates/frontOffice/default/assets/js/order-invoice-after-js-include.html b/local/modules/StripePayment/templates/frontOffice/default/assets/js/order-invoice-after-js-include.html new file mode 100755 index 00000000..58040e79 --- /dev/null +++ b/local/modules/StripePayment/templates/frontOffice/default/assets/js/order-invoice-after-js-include.html @@ -0,0 +1,166 @@ + diff --git a/local/modules/StripePayment/templates/frontOffice/default/assets/js/stripe-js.html b/local/modules/StripePayment/templates/frontOffice/default/assets/js/stripe-js.html new file mode 100755 index 00000000..720ee8e5 --- /dev/null +++ b/local/modules/StripePayment/templates/frontOffice/default/assets/js/stripe-js.html @@ -0,0 +1,29 @@ + + +
+ {if $oneClickPayment} +
+ {intl l="Quick pay" d="stripepayment.fo.default"} +
+ +
+ +
+ + {/if} +
+ {if $oneClickPayment} + {intl l="Or enter card details" d="stripepayment.fo.default"} + {/if} +
+ +
+
+ +
diff --git a/local/modules/StripePayment/templates/frontOffice/default/stripe-paiement.html b/local/modules/StripePayment/templates/frontOffice/default/stripe-paiement.html new file mode 100644 index 00000000..1654133a --- /dev/null +++ b/local/modules/StripePayment/templates/frontOffice/default/stripe-paiement.html @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/local/modules/StripePayment/templates/frontOffice/default2020/assets/css/styles.css b/local/modules/StripePayment/templates/frontOffice/default2020/assets/css/styles.css new file mode 100644 index 00000000..87bff2d1 --- /dev/null +++ b/local/modules/StripePayment/templates/frontOffice/default2020/assets/css/styles.css @@ -0,0 +1,61 @@ +/** + * The CSS shown here will not be introduced in the Quickstart guide, but shows + * how you can use CSS to style your Element's container. + */ +.payment { + margin-bottom: 20px; +} +.stripe-payment { + width: 80%; + margin: auto; + text-align: center; +} +.stripe-payment .payment{ + background-color: #f5f5f5; + border-radius: 5px; +} +.stripe-payment .payment-label { + font-size: 20px; + font-weight: 500; +} +#payment-request-button { + margin-top: 10px; +} +#card-element { + box-sizing: border-box; + + height: 40px; + + padding: 10px 12px; + + border: 1px solid transparent; + border-radius: 4px; + background-color: white; + + box-shadow: 0 1px 3px 0 #e6ebf1; + -webkit-transition: box-shadow 150ms ease; + transition: box-shadow 150ms ease; +} + +#card-element--focus { + box-shadow: 0 1px 3px 0 #cfd7df; +} + +#card-element--invalid { + border-color: #fa755a; +} + +#card-element--webkit-autofill { + background-color: #fefde5 !important; +} +#card-errors, #payment-request-errors { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; + border-radius: .25rem; + padding: .75rem 1.25rem; + text-align: center; +} +#card-errors.hidden, #payment-request-errors.hidden { + display: none; +} \ No newline at end of file diff --git a/local/modules/StripePayment/templates/frontOffice/default2020/assets/js/order-invoice-after-js-include.html b/local/modules/StripePayment/templates/frontOffice/default2020/assets/js/order-invoice-after-js-include.html new file mode 100755 index 00000000..58040e79 --- /dev/null +++ b/local/modules/StripePayment/templates/frontOffice/default2020/assets/js/order-invoice-after-js-include.html @@ -0,0 +1,166 @@ + diff --git a/local/modules/StripePayment/templates/frontOffice/default2020/assets/js/stripe-js.html b/local/modules/StripePayment/templates/frontOffice/default2020/assets/js/stripe-js.html new file mode 100755 index 00000000..720ee8e5 --- /dev/null +++ b/local/modules/StripePayment/templates/frontOffice/default2020/assets/js/stripe-js.html @@ -0,0 +1,29 @@ + + +
+ {if $oneClickPayment} +
+ {intl l="Quick pay" d="stripepayment.fo.default"} +
+ +
+ +
+ + {/if} +
+ {if $oneClickPayment} + {intl l="Or enter card details" d="stripepayment.fo.default"} + {/if} +
+ +
+
+ +
diff --git a/local/modules/StripePayment/templates/frontOffice/default2020/stripe-paiement.html b/local/modules/StripePayment/templates/frontOffice/default2020/stripe-paiement.html new file mode 100644 index 00000000..1654133a --- /dev/null +++ b/local/modules/StripePayment/templates/frontOffice/default2020/stripe-paiement.html @@ -0,0 +1,16 @@ + + \ No newline at end of file