diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d42fd5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*~ +*.iml +.idea/ +/vendor/ +composer.lock diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c88822b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,55 @@ +Licenses +======== + +You may use this package under the terms of either +the New BSD License or the GNU General Public License (GPL) version 2 or 3. + +The BSD License is recommended for most projects. It is easy to understand and it places almost no restrictions on what +you can do with the framework. If the GPL fits better to your project, you can use the package under this license. + +You don't have to notify anyone which license you are using. You can freely use this package in commercial projects +as long as the copyright header remains intact. + + + +New BSD License +--------------- + +Copyright (c) 2015 GrowJOB s.r.o. (http://growjob.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of "Nette Tester" nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" and +any express or implied warranties, including, but not limited to, the implied +warranties of merchantability and fitness for a particular purpose are +disclaimed. In no event shall the copyright owner or contributors be liable for +any direct, indirect, incidental, special, exemplary, or consequential damages +(including, but not limited to, procurement of substitute goods or services; +loss of use, data, or profits; or business interruption) however caused and on +any theory of liability, whether in contract, strict liability, or tort +(including negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. + + + +GNU General Public License +-------------------------- + +GPL licenses are very very long, so instead of including them here we offer +you URLs with full text: + +- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) +- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) diff --git a/README.md b/README.md new file mode 100644 index 0000000..b272e2a --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# MetisFW/Stripe + +WARNING this package is experimental and out-of-date + +[![Downloads this Month](https://img.shields.io/packagist/dm/metisfw/stripe.svg)](https://packagist.org/packages/metisfw/stripe) +[![Latest stable](https://img.shields.io/packagist/v/metisfw/stripe.svg)](https://packagist.org/packages/metisfw/stripe) + +## About + +Stripe payment integration to Nette framework. Internally use [stripe/stripe-php](https://github.com/stripe/stripe-php). + +Inspired by [Kdyby/PayPalExpress](https://github.com/Kdyby/PayPalExpress) + +## Installation +The best way to install is using [Composer](http://getcomposer.org/): + +```sh +$ composer require metisfw/stripe +``` + +## Documentation + +Learn more in the [documentation](https://github.com/MetisFW/Stripe/blob/master/docs/en/index.md). + +There are other classes in this package that are not documented here. +This is because the package is a Nette wrapper of [the official Stripe PHP library](https://github.com/stripe/stripe-php). + +## License + +You may use this package under the terms of either +the New BSD License or the GNU General Public License (GPL) version 2 or 3. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9bcac35 --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "metisfw/stripe", + "type": "library", + "description": "Stripe integration for Nette Framework", + "keywords": ["nette", "stripe", "pay", "checkout", "payment"], + "homepage": "https://github.com/MetisFW", + "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], + "authors": [ + { + "name": "GrowJOB s.r.o", + "email": "dev@growjob.com" + }, + { + "name": "Martin Jonáš", + "email": "martin.jonas@growjob.com" + } + ], + "support": { + "email": "dev@growjob.com", + "issues": "https://github.com/MetisFW/Stripe/issues" + }, + "require": { + "php": ">=5.6.0", + "nette/di": "^2.3", + "nette/application": "^2.3", + "stripe/stripe-php": "^7.45.0" + }, + "require-dev": { + "nette/bootstrap": "^2.3@dev", + "nette/robot-loader": "^2.3@dev", + "nette/tester": "^2.3.2@dev", + "mockery/mockery": "^0.9@dev" + }, + "autoload": { + "psr-0": { + "MetisFW\\Stripe\\": "src/" + } + }, + "archive": { + "exclude": ["tests", ".travis.yml"] + } +} diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 0000000..03c9cf7 --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,152 @@ +# MetisFW/Stripe + +## Setup + +1) Register extension +``` +extensions: + stripe: MetisFW\Stripe\DI\StripeExtension +``` + +2) Set up extension parameters + +```neon +stripe: + secretApiKey: "sk_live_*" + publicApiKey: "pk_live_*" + paymentIntent: + currency: "USD" +``` + +paymentIntent is default config to [Stripe PaymentIntent](https://stripe.com/docs/api/payment_intents/object) + + +##Usage + +##### Sample usage of `PaymentControl` + +###### In Presenter + +```php +use \MetisFW\Stripe\Payment\SimplePaymentOperation; +use \MetisFW\Stripe\UI\PaymentControl; +use Stripe\Api\Payment; +use Nette\Application\UI\Presenter; + +class MyPresenter extends Presenter { + + public function createComponentStripePayment(SimplePaymentOperationFactory $factory) { + $operation = $factory->create('Coffee', 5); + $control = new PaymentControl($operation); + + //set different template if u want to use own + $control->setTemplateFilePath(__DIR__ . './myStripePayment.latte'); + + //called after successfully completed payment proccess + $control->onSuccess[] = function(PaymentControl $control, Payment $paid) { + //something + }; + + //called when payment process fails + $control->onError[] = function(PaymentControl $control) { + //something + }; + + return $control; + } +} +``` + +###### In latte + +```latte +#just +{control stripePayment} + +#or + +#cannot use attributes directly in control +{var attributes = array('class' => 'stripe-payment-button')} +{control stripePayment $attributes, 'Pay me now!'} +``` + +##### Sample usage of `SimplePaymentOperation` + +```php + public function createComponentStripePayment(SimplePaymentOperationFactory $factory) { + $operation = $factory->create('Coffee', 10); + $control = new PaymentControl($operation); + return $control; + } +``` + +##### Sample usage of `PlainPaymentOperation` + +```php +use Stripe\Api\Transaction; + + public function createComponentStripePayment(PlainPaymentOperationFactory $factory) { + // see https://stripe.com/docs/api/payment_intents/object + $params = array( + 'amount' => 5, + 'description' => "Coffeee" + ); + + $operation = $factory->create($params); + $control = new PaymentControl($operation); + return $control; + } +``` + +##### Sample usage of own descendant `\MetisFW\Stripe\Payment\BasePaymentOperation` + +```php +order = $order; + } + + /** + * @return array + */ + protected function getPaymentIntentParams() { + return array( + 'amount' => $order->totalAmount, + 'description' => $order->orderNumber + ); + } + +} + +``` + +###### Events in Operation +```php + public function createComponentStripePaymentButton(FactorType $factory) { + $operation = $factory->create(); + $operation->onSuccess[] = function(PaymentOperation $operation, PaymentIntent $paid) { + //something + } + $operation->onError[] = function(PaymentOperation $operation) { + //something + } + + ... + } +``` diff --git a/src/MetisFW/Stripe/DI/StripeExtension.php b/src/MetisFW/Stripe/DI/StripeExtension.php new file mode 100644 index 0000000..6c4a6f5 --- /dev/null +++ b/src/MetisFW/Stripe/DI/StripeExtension.php @@ -0,0 +1,42 @@ + array() + ); + + public function loadConfiguration() { + $builder = $this->getContainerBuilder(); + $config = $this->getConfig($this->defaults); + + Validators::assertField($config, 'publicApiKey', 'string'); + Validators::assertField($config, 'secretApiKey', 'string'); + Validators::assertField($config, 'paymentIntent', 'array'); + + $builder->addDefinition($this->prefix('Stripe')) + ->setClass('MetisFW\Stripe\StripeContext', array($config['publicApiKey'], $config['secretApiKey'])) + ->addSetup('setPaymentIntentDefaults', array($config['paymentIntent'])); + + } + + /** + * @param Configurator $configurator + */ + public static function register(Configurator $configurator) { + $configurator->onCompile[] = function ($config, Compiler $compiler) { + $compiler->addExtension('stripe', new StripeExtension()); + }; + } + +} diff --git a/src/MetisFW/Stripe/Payment/BasePaymentOperation.php b/src/MetisFW/Stripe/Payment/BasePaymentOperation.php new file mode 100644 index 0000000..cfee1ad --- /dev/null +++ b/src/MetisFW/Stripe/Payment/BasePaymentOperation.php @@ -0,0 +1,69 @@ +context = $context; + } + + /** + * @return PaymentIntent + */ + public function createPaymentIntent() { + return $this->context->createPaymentIntent($this->getPaymentIntentParams()); + } + + public function getPublicApiKey() { + return $this->context->getPublicApiKey(); + } + + /** + * @return array + */ + abstract protected function getPaymentIntentParams(); + + /** + * @param string $paymentIntentId + * @return PaymentIntent + */ + public function handleSuccess($paymentIntentId) { + try { + $payment = $this->context->retrievePaymentIntent($paymentIntentId); + if($payment->status != PaymentIntent::STATUS_SUCCEEDED) { + throw new StripeException("Unexpected PaymentIntent status " + $payment->status + " for $paymentIntentId"); + } + $this->onSuccess($this, $payment); + return $payment; + } + catch(\Exception $exception) { + Debugger::log($exception, Debugger::EXCEPTION); + throw $exception; + } + } + +} diff --git a/src/MetisFW/Stripe/Payment/PaymentOperation.php b/src/MetisFW/Stripe/Payment/PaymentOperation.php new file mode 100644 index 0000000..8f5b9e5 --- /dev/null +++ b/src/MetisFW/Stripe/Payment/PaymentOperation.php @@ -0,0 +1,26 @@ +params = $params; + } + + /** + * @return array + */ + protected function getPaymentIntentParams() { + return $this->params; + } + +} diff --git a/src/MetisFW/Stripe/Payment/PlainPaymentOperationFactory.php b/src/MetisFW/Stripe/Payment/PlainPaymentOperationFactory.php new file mode 100644 index 0000000..bd192cb --- /dev/null +++ b/src/MetisFW/Stripe/Payment/PlainPaymentOperationFactory.php @@ -0,0 +1,13 @@ +name = $name; + $this->quantity = $quantity; + $this->price = $price; + $this->currency = $currency; + } + + /** + * @return array + */ + protected function getPaymentIntentParams() { + $params = [ + 'amount' => $this->price * $this->quantity, + 'description' => $this->name + ]; + if($this->currency) { + $params['currency'] = $this->currency; + } + return $params; + } + +} \ No newline at end of file diff --git a/src/MetisFW/Stripe/Payment/SimplePaymentOperationFactory.php b/src/MetisFW/Stripe/Payment/SimplePaymentOperationFactory.php new file mode 100644 index 0000000..c92c7f7 --- /dev/null +++ b/src/MetisFW/Stripe/Payment/SimplePaymentOperationFactory.php @@ -0,0 +1,17 @@ +publicApiKey = $publicApiKey; + $this->secretApiKey = $secretApiKey; + } + + /** + * @param array $params + */ + public function setPaymentIntentDefaults(array $params) { + $this->paymentIntentDefaults = $params; + } + + /** + * @return PaymentIntent + * @throws StripeException + */ + public function createPaymentIntent(array $params) { + try { + Stripe::setApiKey($this->secretApiKey); + $params = array_merge_recursive($this->paymentIntentDefaults, $params); + return PaymentIntent::create($params); + } catch (ApiErrorException $exception) { + throw new StripeException("Create PaymentIntent failed", 0, $exception); + } + } + + /** + * @return PaymentIntent + * @throws StripeException + */ + public function retrievePaymentIntent(string $paymentIntentId) { + try { + Stripe::setApiKey($this->secretApiKey); + return PaymentIntent::retrieve($paymentIntentId); + } catch (ApiErrorException $exception) { + throw new StripeException("Retrieve PaymentIntent failed", 0, $exception); + } + } + + /** + * @return string + */ + public function getPublicApiKey() { + return $this->publicApiKey; + } + +} diff --git a/src/MetisFW/Stripe/StripeException.php b/src/MetisFW/Stripe/StripeException.php new file mode 100644 index 0000000..4c47554 --- /dev/null +++ b/src/MetisFW/Stripe/StripeException.php @@ -0,0 +1,7 @@ +operation = $operation; + } + + public function setTemplateFilePath($templateFilePath) { + $this->templateFilePath = $templateFilePath; + } + public function getTemplateFilePath() { + return $this->templateFilePath ? $this->templateFilePath : $this->getDefaultTemplateFilePath(); + } + + public function handleSuccess() { + $paymentIntentId = $this->getPresenter()->getParameter('paymentIntentId'); + + try { + $paidPayment = $this->operation->handleSuccess($paymentIntentId); + } + catch(StripeException $exception) { + $this->errorHandler($exception); + return; + } + + $this->onSuccess($this, $paidPayment); + } + + /** + * @param array $attrs + * @param string $text + */ + public function render($attrs = array(), $text = "Submit payment") { + $template = $this->template; + $templateFilePath = $this->getTemplateFilePath(); + $template->setFile($templateFilePath); + try { + $template->clientSecret = $this->operation->createPaymentIntent()->client_secret; + $template->publicApiKey = $this->operation->getPublicApiKey(); + } catch (StripeException $exception) { + $this->errorHandler($exception); + } + $template->text = $text; + $template->attrs = $attrs; + $template->elementId = Random::generate(5); + $template->successLink = $this->link('//success!'); + $template->render(); + } + + /** + * @param \Exception $exception + * + * @throws StripeException + * + * @return void + */ + protected function errorHandler(\Exception $exception) { + if(!$this->onError) { + throw $exception; + } + + $this->onError($this, $exception); + } + + protected function getDefaultTemplateFilePath() { + return __DIR__.'/templates/PaymentControl.latte'; + } + +} diff --git a/src/MetisFW/Stripe/UI/templates/PaymentControl.latte b/src/MetisFW/Stripe/UI/templates/PaymentControl.latte new file mode 100644 index 0000000..6839123 --- /dev/null +++ b/src/MetisFW/Stripe/UI/templates/PaymentControl.latte @@ -0,0 +1,9 @@ +
+ +
+ +
+ \ No newline at end of file